diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 00000000..a1aa35b6 --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,96 @@ +environment: + matrix: + - job_name: win + appveyor_build_worker_image: Visual Studio 2022 + + - job_name: linux + appveyor_build_worker_image: Ubuntu1604 + + - job_name: mac + appveyor_build_worker_image: macos-catalina + +matrix: + fast_finish: true + +version: build-{build} + +configuration: Release + +platform: x64 + +clone_depth: 1 + +init: + - ps: >- + $env:DATE = $(Get-Date -Format d-MMM-yyyy) + + $githash = $env:APPVEYOR_REPO_COMMIT.Substring(0, 7) + + $gittag = if ($env:APPVEYOR_REPO_TAG -eq $True) {"_$($env:APPVEYOR_REPO_TAG_NAME)"} else {""} + + Update-AppveyorBuild -Version "$($env:DATE)_g${githash}${gittag}" + + $env:RELEASE_VERSION = $(Get-Date -Format d-MMMM-yyyy) + +for: + - + matrix: + only: + - job_name: win + + build_script: + - cmake -Wno-dev -DZLIB_IMPLEMENTATION=Cloudflare -DUSE_OPENJPEG=GitHub -DUSE_JPEGLS=ON -B build + - cmake --build build --config %configuration% + + after_build: + - 7z a dcm2niix_win.zip .\build\bin\* >$null + - appveyor PushArtifact dcm2niix_win.zip + + - + matrix: + only: + - job_name: linux + + build_script: + - export CC=gcc-8 CXX=g++-8 + - cmake -Wno-dev -DZLIB_IMPLEMENTATION=Cloudflare -DUSE_OPENJPEG=GitHub -DUSE_JPEGLS=ON -B build + - cmake --build build + + after_build: + - strip -sx build/bin/* + - 7z a dcm2niix_lnx.zip ./build/bin/* &>/dev/null + - appveyor PushArtifact dcm2niix_lnx.zip + + - + matrix: + only: + - job_name: mac + + build_script: + - sudo xcode-select -s /Applications/Xcode-11.3.1.app + - cmake -Wno-dev -DCMAKE_OSX_ARCHITECTURES=x86_64 -DZLIB_IMPLEMENTATION=Cloudflare -DUSE_OPENJPEG=GitHub -DUSE_JPEGLS=ON -B intel + - cmake --build intel + - sudo xcode-select -s /Applications/Xcode-12.3.app + - cmake -Wno-dev -DCMAKE_OSX_ARCHITECTURES=arm64 -DZLIB_IMPLEMENTATION=Cloudflare -DUSE_OPENJPEG=GitHub -DUSE_JPEGLS=ON -B apple + - cmake --build apple + + after_build: + - mkdir -p build/bin + - lipo -create -output build/bin/dcm2niix intel/bin/dcm2niix apple/bin/dcm2niix + - strip -Sx build/bin/* + - 7z a dcm2niix_macos.zip ./build/bin/* &>/dev/null + - appveyor PushArtifact dcm2niix_macos.zip + +deploy: + - provider: GitHub + tag: $(APPVEYOR_REPO_TAG_NAME) + release: version $(RELEASE_VERSION) ($(APPVEYOR_REPO_TAG_NAME)) + description: "" + auth_token: + secure: gCltVLQEWsjSTRlsi8qw7FGP54ujBq60apjXkWTV954b65bOHl95hXMxxkQ734L4 + artifact: /dcm2niix_.*\.zip/ + draft: false + prerelease: false + on: + branch: master + APPVEYOR_REPO_TAG: true diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 00000000..a9022c82 --- /dev/null +++ b/.codespellrc @@ -0,0 +1,11 @@ +[codespell] +skip = .git,*.json,dcm_qa* +# te - the TE used in the code often +# clen - another common variable for length of smth +# tage - for \tAge, inline ignores are yet to be released +# nd - there is some kind of ND whi +# ❯ grep -e 'trace or MD ' -e 'Trace/ND' ./console/nii_dicom_batch.cpp +# // the isotropic trace or MD can be calculated) often come as +# /*if (!dcmList[indx0].isDerived) //no need to warn if images are derived Trace/ND pair +# ser - used in printMessage(" acq %d img %d ser %ld ... +ignore-words-list = te,clen,tage,nd,ser diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml new file mode 100644 index 00000000..c26aab66 --- /dev/null +++ b/.github/workflows/codespell.yml @@ -0,0 +1,19 @@ +--- +name: Codespell + +on: + push: + branches: [development,master] + pull_request: + branches: [development,master] + +jobs: + codespell: + name: Check for spelling errors + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Codespell + uses: codespell-project/actions-codespell@v1 diff --git a/.gitignore b/.gitignore index 0b9be923..0b86f87c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,14 @@ /build/ /bin/ /console/dcm2niix +# Python wrapper +*.py[co] +*.so +__pycache__/ +/_skbuild/ +/_cmake_test_compile/ +/dcm2niix/_dist_ver.py +/dcm2niix/dcm2niix +MANIFEST +/*.egg*/ +/dist/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8775c604..00000000 --- a/.travis.yml +++ /dev/null @@ -1,47 +0,0 @@ -language: cpp - -git: - depth: 1 - -matrix: - include: - - os: linux - dist: trusty - addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - g++-5 # support c++14 - env: - - MATRIX_EVAL="CC=gcc-5 && CXX=g++-5" - - TARGET=lnx - - os: osx - osx_image: xcode8.3 # Tracvis default: OS X 10.12.6 and Xcode 8.3.3 - env: TARGET=mac - -before_install: - - eval "${MATRIX_EVAL}" - - git submodule update --init --remote --depth=3 - -script: - # - mkdir build && cd build && cmake -DBATCH_VERSION=ON -DUSE_OPENJPEG=ON -DUSE_JPEGLS=true -DZLIB_IMPLEMENTATION=Cloudflare .. && make && cd - - - mkdir build && cd build && cmake -DBATCH_VERSION=OFF -DUSE_OPENJPEG=ON -DUSE_JPEGLS=true -DZLIB_IMPLEMENTATION=Cloudflare .. && make && cd - - - export PATH=$PWD/build/bin:$PATH - - cd dcm_qa && ./batch.sh && cd - - - cd dcm_qa_nih && ./batch.sh && cd - - - cd dcm_qa_uih && ./batch.sh && cd - - -before_deploy: - - zip -j dcm2niix_${TARGET}.zip build/bin/* - - sleep 300 # make sure appveyor deployment is done, thus proper release name is set - -deploy: - provider: releases - api_key: - secure: sVIYRakcEQdMPEdGSSePtMVCMQvaohqV7NNzEErAgZ+b/4ofv2aPpJb5kNTv3JRl2FrPy7iXJ8lOUQ/95pqvimX6jv5ztksTNXtSMnHZNbjjWwIc99enPY+mSdWMO2lb9vGBWQ9GNfXjmk7MgtDHPjjygbuZfUw9fmGy4ocxkws= - file_glob: true - file: dcm2niix*.zip - skip_cleanup: true - on: - tags: true diff --git a/BIDS/README.md b/BIDS/README.md index d018fa5d..37200c58 100644 --- a/BIDS/README.md +++ b/BIDS/README.md @@ -140,8 +140,9 @@ Fields specific to MRI scans. | EstimatedEffectiveEchoSpacing | s | | D | | EstimatedTotalReadoutTime | s | | D | | FlipAngle | deg | DICOM tag 0018,1314 | B | +| VariableFlipAngleFlag | b | DICOM tag 0018,1315 | D | | ImageOrientationPatientDICOM | | DICOM tag 0020,0037 | D | -| ImagingFrequency | MHz | DICOM tag 0018,0084 | D | +| ImagingFrequency | MHz | DICOM tag 0018,0084 or 0018,9098 | D | | InPlanePhaseEncodingDirectionDICOM | | DICOM tag 0018,1312 | D | | InversionTime | s | DICOM tag 0018,0082 | B | | MagneticFieldStrength | T | DICOM tag 0018,0087 | B | @@ -151,19 +152,21 @@ Fields specific to MRI scans. | NumberOfAverages | | DICOM tag 0018,0083 | D | | ParallelAcquisitionTechnique | | DICOM tag 0018, 9078, aka `SENSE`, `GRAPPA` | B | | ParallelReductionFactorInPlane | | DICOM tag 0018,9069 | B | -| ParallelReductionOutOfPlane | | DICOM tag 0018,9155 | D | +| ParallelReductionFactorOutOfPlane | | DICOM tag 0018,9155 | B | | PartialFourierDirection | | DICOM tag 0018,9036 | B | | PercentPhaseFOV | | DICOM tag 0018,0094 | D | | PercentSampling | | DICOM tag 0018,0093 | D | | PhaseEncodingAxis | | When polarity unknown | B | | PhaseEncodingDirection | | When polarity known | B | -| PhaseEncodingSteps | | DICOM tag 0018,0089 | D | +| FrequencyEncodingSteps | | DICOM tag 0018,9058 | D | +| PhaseEncodingSteps | | DICOM tag 0018,0089 or 0018,9231 aka PhaseEncodingStepsInPlane | D | +| PhaseEncodingStepsOutOfPlane | | DICOM tag 0018,9232 | D | | PixelBandwidth | Hz | DICOM tag 0018,0095 | D | | ReceiveCoilName | | DICOM tag 0018,1250 | B | | RepetitionTime | s | DICOM tag 0018,0080 | B | | RepetitionTimeExcitation | s | DICOM tag 0018, 0080 for some manufacturers | B | | RepetitionTimeInversion | s | | D | -| SAR | | DICOM tag 0018,1316 | D | +| SAR | | DICOM tag 0018,1316 or 0018,9181 defined by 0018,9179 | D | | SliceThickness | mm | [nb](http://dclunie.blogspot.com/2013/10/how-thick-am-i-sad-story-of-lonely-slice.html) | D | | SliceTiming | s | | B | | SpacingBetweenSlices | mm | [nb](http://dclunie.blogspot.com/2013/10/how-thick-am-i-sad-story-of-lonely-slice.html) | D | @@ -228,6 +231,9 @@ Data unique to [GE](https://github.com/rordenlab/dcm2niix/tree/master/GE). Deter | ASLContrastTechnique | | DICOM tag 0043,10A3 | D | | ASLLabelingTechnique | | DICOM tag 0043,10A4 | D | | LabelingDuration | s | DICOM tag 0043,10A5 | B | +| SliceTiming | s | [see notes](https://github.com/rordenlab/dcm2niix/tree/master/GE#slice-timing) | B | +| CompressedSensingFactor | | DICOM tag 0043,10b7 | D | +| DeepLearningFactor | | DICOM tag 0043,10ca | D | ### Manufacturer Philips @@ -298,6 +304,7 @@ Fields specific to Siemens V*-series (e.g. VB, VE) MRI systems (e.g. Verio, Trio | DelayTime | s | Pause between EPI volumes, where TR is longer than required by TA (`sparse` imaging) | D | | TxRefAmp | V | | D | | ParallelReductionFactorInPlane | | DICOM tag 0021,1009 | B | +| ParallelReductionFactorOutOfPlane | | CSA `sPat.lAccelFact3D` | B | | PhaseResolution | f | | D | | PhaseOversampling | | | D | | MultibandAccelerationFactor | | DICOM tag 0021,1009 | B | @@ -308,7 +315,6 @@ Fields specific to Siemens V*-series (e.g. VB, VE) MRI systems (e.g. Verio, Trio | FmriExternalInfo | | | D | | WipMemBlock | | | D | | AveragesDouble | | CSA `dAveragesDouble`, fractions possible, independent of DICOM `NumberOfAverages` (0018,0083) | D | -| AccelFact3D | | 3D Acquisitions (Parallel Reduction Factor Across Slices) | D | | ProtocolName | | Check SeriesDescription - they might be switched around | D | | RefLinesPE | | # of reference lines in the phase encoding direction for acceleration (GRAPPA) | D | | ConsistencyInfo | | The more complete software version, e.g. VE11C or VE11E instead of just VE11. | D | @@ -321,15 +327,19 @@ Fields specific to Siemens V*-series (e.g. VB, VE) MRI systems (e.g. Verio, Trio ### Manufacturer Siemens Magnetic Resonance Imaging (XA) -Fields specific to Siemens XA-series MRI systems (Sola, Vida). +Fields specific to [Siemens XA-series](https://github.com/rordenlab/dcm2niix/tree/master/Siemens#siemens-x-series) MRI systems (Sola, Vida). -| Field | Unit | Comments | Defined By | -|------------------------------|------|------------------------|------------| -| ReceiveCoilActiveElements | | DICOM tag 0021,114F | B | -| BandwidthPerPixelPhaseEncode | Hz | DICOM tag 0021,1153 | D | -| ScanningSequence | | DICOM tag 0021,105a | D | -| PostLabelDelay | s | DICOM tag 0018,9258 | D | -| NonlinearGradientCorrection | b | 0008,0008 or 0021,1175 | B | +| Field | Unit | Comments | Defined By | +|------------------------------|------|----------------------------|------------| +| ReceiveCoilActiveElements | | DICOM tag 0021,114F | B | +| BandwidthPerPixelPhaseEncode | Hz | DICOM tag 0021,1153 | D | +| ScanningSequence | | DICOM tag 0021,105a | D | +| PostLabelDelay | s | DICOM tag 0018,9258 | D | +| NonlinearGradientCorrection | b | 0008,0008 or 0021,1175 | B | +| PhaseEncodingDirection | | polarity from 0021,111c | B | +| SpoilingState | | 0021,105B | B | + +Siemens also includes some sequence information in the private MRPhoenixProtocol (0021,1019) tag. You can view this with [gdcmdump](https://gdcm.sourceforge.net/html/gdcmdump.html), e.g. `gdcmdump --mrprotocol img.dcm`. Fields that dcm2niix inspects include `sPat.lAccelFact3D `, `sPat.lAccelFactPE`, `sPat.lRefLinesPE` and `sPat.ucPATMode`. The behavior of dcm2niix will be more well documented as our understanding of this tag improves. ### Manufacturer UIH diff --git a/COMPILE.md b/COMPILE.md index b8bcc99b..7e2fd146 100644 --- a/COMPILE.md +++ b/COMPILE.md @@ -8,12 +8,14 @@ Beyond the complexity of compiling the software, the only downside to adding opt The text below generally describes how to build dcm2niix using the [GCC](https://gcc.gnu.org) compiler using the `g++` command. However, the code is portable and you can use different compilers. For [clang/llvm](https://clang.llvm.org) compile using `clang++`. If you have the [Intel C compiler](https://software.intel.com/en-us/c-compilers), you can substitute the `icc` command. The code is compatible with Microsoft's VS 2015 or later. For [Microsoft's C compiler](http://landinghub.visualstudio.com/visual-cpp-build-tools) you would use the `cl` command. In theory, the code should support other compilers, but this has not been tested. Be aware that if you do not have gcc installed the `g++` command may use a default to a compiler (e.g. clang). To check what compiler was used, run the dcm2niix software: it always reports the version and the compiler used for the build. +Note that in the commands below we increase the [stack size](https://stackoverflow.com/questions/18909395/how-do-i-increase-the-stack-size-when-compiling-with-clang-on-os-x)zgit to 16mb, which is larger than the Unix (8mb) and Windows (1mb) defaults. + ## Building the command line version without cmake You can also build the software without C-make. The easiest way to do this is to run the function "make" from the "console" folder. Note that this only creates the default version of dcm2niix, not the optional batch version described above. The make command simply calls the g++ compiler, and if you want you can tune this for your build. In essence, the make function simply calls ``` -g++ -dead_strip -O3 -I. main_console.cpp nii_dicom.cpp jpg_0XC3.cpp ujpeg.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp nii_foreign.cpp -o dcm2niix -DmyDisableOpenJPEG +g++ -O3 -I. main_console.cpp nii_dicom.cpp jpg_0XC3.cpp ujpeg.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp nii_foreign.cpp -o dcm2niix -DmyDisableOpenJPEG -Wl,-stack_size -Wl,3f00000 ``` The following sub-sections list how you can modify this basic recipe for your needs. @@ -31,7 +33,7 @@ cmake -DUSE_OPENJPEG=ON -DCMAKE_CXX_FLAGS=-g .. && make If we have zlib, we can use it (-lz) and disable [miniz](https://code.google.com/p/miniz/) (-myDisableMiniZ) ``` -g++ -O3 -DmyDisableOpenJPEG -I. main_console.cpp nii_dicom.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp jpg_0XC3.cpp ujpeg.cpp nii_foreign.cpp -dead_strip -o dcm2niix -lz -DmyDisableMiniZ +g++ -O3 -DmyDisableOpenJPEG -I. main_console.cpp nii_dicom.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp jpg_0XC3.cpp ujpeg.cpp nii_foreign.cpp -o dcm2niix -lz -DmyDisableMiniZ g++ -O3 -I. main_console.cpp nii_dicom.cpp jpg_0XC3.cpp ujpeg.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp nii_foreign.cpp -o dcm2niix -DmyDisableOpenJPEG -Wl,-stack_size -Wl,3f00000 ``` ##### MINGW BUILD @@ -39,7 +41,7 @@ g++ -O3 -DmyDisableOpenJPEG -I. main_console.cpp nii_dicom.cpp nifti1_io_core.cp If you use the (obsolete) compiler MinGW on Windows you will want to include the rare libgcc libraries with your executable so others can use it. Here I also demonstrate the optional "-DmyDisableZLib" to remove zip support. ``` -g++ -O3 -s -DmyDisableOpenJPEG -DmyDisableZLib -I. main_console.cpp nii_dicom.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp jpg_0XC3.cpp ujpeg.cpp nii_foreign.cpp -o dcm2niix -static-libgcc +g++ -O3 -s -DmyDisableOpenJPEG -DmyDisableZLib -I. main_console.cpp nii_dicom.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp jpg_0XC3.cpp ujpeg.cpp nii_foreign.cpp -o dcm2niix -static-libgcc g++ -O3 -I. main_console.cpp nii_dicom.cpp jpg_0XC3.cpp ujpeg.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp nii_foreign.cpp -o dcm2niix -DmyDisableOpenJPEG -Wl,-stack_size -Wl,3f00000 ``` ##### DISABLING CLASSIC JPEG @@ -47,7 +49,7 @@ g++ -O3 -s -DmyDisableOpenJPEG -DmyDisableZLib -I. main_console.cpp nii_dicom.cp DICOM images can be stored as either raw data or compressed using one of many formats as described by the [transfer syntaxes](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Transfer_Syntaxes_and_Compressed_Images). One of the compressed formats is the lossy classic JPEG format (which is separate from and predates the lossy JPEG 2000 format). This software comes with the [NanoJPEG](http://keyj.emphy.de/nanojpeg/) library to handle these images. However, you can use the `myDisableClassicJPEG` compiler switch to remove this dependency. The resulting executable will be smaller but will not be able to convert images stored with this format. ``` -g++ -dead_strip -O3 -I. main_console.cpp nii_dicom.cpp jpg_0XC3.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp nii_foreign.cpp -o dcm2niix -DmyDisableClassicJPEG -DmyDisableOpenJPEG +g++ -O3 -I. main_console.cpp nii_dicom.cpp jpg_0XC3.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp nii_foreign.cpp -o dcm2niix -DmyDisableClassicJPEG -DmyDisableOpenJPEG g++ -O3 -I. main_console.cpp nii_dicom.cpp jpg_0XC3.cpp ujpeg.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp nii_foreign.cpp -o dcm2niix -DmyDisableOpenJPEG -Wl,-stack_size -Wl,3f00000 ``` ##### USING LIBJPEG-TURBO TO DECODE CLASSIC JPEG @@ -55,18 +57,18 @@ g++ -dead_strip -O3 -I. main_console.cpp nii_dicom.cpp jpg_0XC3.cpp nifti1_io_co By default, classic JPEG images will be decoded using the [compact NanoJPEG decoder](http://keyj.emphy.de/nanojpeg/). However, the compiler directive `myTurboJPEG` will create an executable based on the [libjpeg-turbo](http://www.libjpeg-turbo.org) library. This library is a faster decoder and is the standard for many Linux distributions. On the other hand, the lossy classic JPEG is rarely used for DICOM images, so this compilation has extra dependencies and can result in a larger executable size (for static builds). ``` -g++ -dead_strip -O3 -I. main_console.cpp nii_dicom.cpp jpg_0XC3.cpp ujpeg.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp nii_foreign.cpp -o dcm2niix -DmyDisableOpenJPEG -DmyTurboJPEG -I/opt/libjpeg-turbo/include /opt/libjpeg-turbo/lib/libturbojpeg.a +g++ -O3 -I. main_console.cpp nii_dicom.cpp jpg_0XC3.cpp ujpeg.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp nii_foreign.cpp -o dcm2niix -DmyDisableOpenJPEG -DmyTurboJPEG -I/opt/libjpeg-turbo/include /opt/libjpeg-turbo/lib/libturbojpeg.a g++ -O3 -I. main_console.cpp nii_dicom.cpp jpg_0XC3.cpp ujpeg.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp nii_foreign.cpp -o dcm2niix -DmyDisableOpenJPEG -Wl,-stack_size -Wl,3f00000 ``` ##### JPEG-LS BUILD You can compile dcm2niix to convert DICOM images compressed with the [JPEG-LS](https://en.wikipedia.org/wiki/JPEG_2000) [transfer syntaxes 1.2.840.10008.1.2.4.80 and 1.2.840.10008.1.2.4.81](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Transfer_Syntaxes_and_Compressed_Images). Decoding this format is handled by the [CharLS library](https://github.com/team-charls/charls), which is included with dcm2niix in the `charls` folder. The included code was downloaded from the CharLS website on 6 June 2018. To enable support you will need to include the `myEnableJPEGLS` compiler flag as well as a few file sin the `charls` folder. Therefore, a minimal compile (with just JPEG-LS and without JPEG2000) should look like this: -`g++ -I. -DmyEnableJPEGLS charls/jpegls.cpp charls/jpegmarkersegment.cpp charls/interface.cpp charls/jpegstreamwriter.cpp charls/jpegstreamreader.cpp main_console.cpp nii_foreign.cpp nii_dicom.cpp jpg_0XC3.cpp ujpeg.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp -o dcm2niix -DmyDisableOpenJPEG` - -Alternatively, you can decompress an image in JPEG-LS to an uncompressed DICOM using [gdcmconv](https://github.com/malaterre/GDCM)(e.g. `gdcmconv -w 3691459 3691459.dcm`). Or you can use gdcmconv compress a DICOM to JPEG-LS (e.g. `gdcmconv -L 3691459 3691459.dcm`). Alternatively, the DCMTK tool [dcmcjpls](https://support.dcmtk.org/docs/dcmcjpls.html) provides JPEG-LS support. - +``` +g++ -I. -DmyEnableJPEGLS charls/jpegls.cpp charls/jpegmarkersegment.cpp charls/interface.cpp charls/jpegstreamwriter.cpp charls/jpegstreamreader.cpp main_console.cpp nii_foreign.cpp nii_dicom.cpp jpg_0XC3.cpp ujpeg.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp -o dcm2niix -DmyDisableOpenJPEG g++ -O3 -I. main_console.cpp nii_dicom.cpp jpg_0XC3.cpp ujpeg.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp nii_foreign.cpp -o dcm2niix -DmyDisableOpenJPEG -Wl,-stack_size -Wl,3f00000 +``` +Alternatively, you can decompress an image in JPEG-LS to an uncompressed DICOM using [gdcmconv](https://github.com/malaterre/GDCM) (e.g. `gdcmconv -w 3691459 3691459.dcm`). Or you can use gdcmconv compress a DICOM to JPEG-LS (e.g. `gdcmconv -L 3691459 3691459.dcm`). Alternatively, the DCMTK tool [dcmcjpls](https://support.dcmtk.org/docs/dcmcjpls.html) provides JPEG-LS support. ##### JPEG2000 BUILD @@ -81,53 +83,45 @@ You can build dcm2niix with JPEG2000 decompression support using OpenJPEG 2.1.0. You should then be able to run: ``` -g++ -O3 -dead_strip -I. main_console.cpp nii_dicom.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp jpg_0XC3.cpp ujpeg.cpp nii_foreign.cpp -o dcm2niix -lopenjp2 +g++ -O3 -I. main_console.cpp nii_dicom.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp jpg_0XC3.cpp ujpeg.cpp nii_foreign.cpp -o dcm2niix -lopenjp2 g++ -O3 -I. main_console.cpp nii_dicom.cpp jpg_0XC3.cpp ujpeg.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp nii_foreign.cpp -o dcm2niix -DmyDisableOpenJPEG -Wl,-stack_size -Wl,3f00000 ``` But in my experience this works best if you explicitly tell the software how to find the libraries, so your compile will probably look like one of these options: ``` #for MacOS -g++ -O3 -dead_strip -I. main_console.cpp nii_dicom.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp jpg_0XC3.cpp ujpeg.cpp nii_foreign.cpp -o dcm2niix -I/usr/local/include/openjpeg-2.1 /usr/local/lib/libopenjp2.a -``` -``` +g++ -O3 -I. main_console.cpp nii_dicom.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp jpg_0XC3.cpp ujpeg.cpp nii_foreign.cpp -o dcm2niix -I/usr/local/include/openjpeg-2.1 /usr/local/lib/libopenjp2.a g++ -O3 -I. main_console.cpp nii_dicom.cpp jpg_0XC3.cpp ujpeg.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp nii_foreign.cpp -o dcm2niix -DmyDisableOpenJPEG -Wl,-stack_size -Wl,3f00000 #For older Linux -g++ -O3 -I. main_console.cpp nii_dicom.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp jpg_0XC3.cpp ujpeg.cpp nii_foreign.cpp -o dcm2niix -I/usr/local/lib /usr/local/lib/libopenjp2.a -``` -``` +g++ -O3 -I. main_console.cpp nii_dicom.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp jpg_0XC3.cpp ujpeg.cpp nii_foreign.cpp -o dcm2niix -I/usr/local/lib /usr/local/lib/libopenjp2.a g++ -O3 -I. main_console.cpp nii_dicom.cpp jpg_0XC3.cpp ujpeg.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp nii_foreign.cpp -o dcm2niix -DmyDisableOpenJPEG -Wl,-stack_size -Wl,3f00000 #For modern Linux -g++ -O3 -s -I. main_console.cpp nii_dicom.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp jpg_0XC3.cpp ujpeg.cpp nii_foreign.cpp -lpthread -o dcm2niix -I/usr/local/include/openjpeg-2.2 ~/openjpeg-master/build/bin/libopenjp2.a +g++ -O3 -s -I. main_console.cpp nii_dicom.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp jpg_0XC3.cpp ujpeg.cpp nii_foreign.cpp -lpthread -o dcm2niix -I/usr/local/include/openjpeg-2.2 ~/openjpeg-master/build/bin/libopenjp2.a g++ -O3 -I. main_console.cpp nii_dicom.cpp jpg_0XC3.cpp ujpeg.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp nii_foreign.cpp -o dcm2niix -DmyDisableOpenJPEG -Wl,-stack_size -Wl,3f00000 ``` If you want to build this with JPEG2000 decompression support using Jasper: You will need to have the Jasper (http://www.ece.uvic.ca/~frodo/jasper/) and libjpeg (http://www.ijg.org) libraries installed which for Linux users may be as easy as running 'sudo apt-get install libjasper-dev' (otherwise, see http://www.ece.uvic.ca/~frodo/jasper/#doc). You can then run: ``` -g++ -O3 -DmyDisableOpenJPEG -DmyEnableJasper -I. main_console.cpp nii_dicom.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp jpg_0XC3.cpp ujpeg.cpp nii_foreign.cpp -s -o dcm2niix -ljasper -ljpeg +g++ -O3 -DmyDisableOpenJPEG -DmyEnableJasper -I. main_console.cpp nii_dicom.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp jpg_0XC3.cpp ujpeg.cpp nii_foreign.cpp -s -o dcm2niix -ljasper -ljpeg g++ -O3 -I. main_console.cpp nii_dicom.cpp jpg_0XC3.cpp ujpeg.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp nii_foreign.cpp -o dcm2niix -DmyDisableOpenJPEG -Wl,-stack_size -Wl,3f00000 ``` ##### VISUAL STUDIO BUILD -This software can be compiled with VisualStudio 2015. This example assumes the compiler is in your path. +This software can be compiled with [Microsoft's Visual Studio C compiler](http://landinghub.visualstudio.com/visual-cpp-build-tools). This example assumes the compiler is in your path (For Windows 11 you can run the `x64 Native Tools Command Prompt`). + +Crucially, you will want to [set a large stack allocation](https://learn.microsoft.com/en-us/cpp/build/reference/stack-stack-allocations?view=msvc-170). This allows dcm2niix to convert a huge number of DICOM images in a single pass (which requires a large amount of memory). ``` -vcvarsall amd64 -cl /EHsc main_console.cpp nii_dicom.cpp jpg_0XC3.cpp ujpeg.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp nii_foreign.cpp -DmyDisableOpenJPEG /o dcm2niix +cl /wd4018 /wd4068 /wd4101 /wd4244 /wd4267 /wd4305 /wd4308 /wd4334 /wd4800 /wd4819 /wd4996 base64.cpp cJSON.cpp main_console.cpp nii_foreign.cpp nii_dicom.cpp jpg_0XC3.cpp ujpeg.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp /Fe:dcm2niix.exe -DmyDisableOpenJPEG /link /STACK:8388608 ``` -##### OSX BUILD WITH BOTH 32 AND 64-BIT SUPPORT +##### MacOS BUILD UNIVERSAl BINARIES SUPPORT -Building command line version universal binary from OSX 64 bit system: - This requires a C compiler. With a terminal, change directory to the 'conosle' folder and run the following: +On MacOS you can create Universal binaries, that bundle optimized code for different architectures. For example, supporting PowerPC, Intel and Apple Silicon (e.g. M1) CPUs. Further, you can optimize Intel code for either 32-bit or 64-bit operation. More details on Universal binaries and notarization is provided [here](https://github.com/neurolabusc/NotarizeC). -``` -g++ -O3 -DmyDisableOpenJPEG -I. main_console.cpp nii_dicom.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp jpg_0XC3.cpp ujpeg.cpp nii_foreign.cpp -dead_strip -arch i386 -o dcm2niix32 -``` - -``` -g++ -O3 -DmyDisableOpenJPEG -I. main_console.cpp nii_dicom.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp jpg_0XC3.cpp ujpeg.cpp nii_foreign.cpp -dead_strip -o dcm2niix64 -``` +Here is a simple example of creating independent 32-bit and 64-bit executables and then using `lipo` to create a single universal executable: ``` +g++ -O3 -DmyDisableOpenJPEG -I. main_console.cpp nii_dicom.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp jpg_0XC3.cpp ujpeg.cpp nii_foreign.cpp -arch i386 -o dcm2niix32 g++ -O3 -I. main_console.cpp nii_dicom.cpp jpg_0XC3.cpp ujpeg.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp nii_foreign.cpp -o dcm2niix -DmyDisableOpenJPEG -Wl,-stack_size -Wl,3f00000 +g++ -O3 -DmyDisableOpenJPEG -I. main_console.cpp nii_dicom.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp jpg_0XC3.cpp ujpeg.cpp nii_foreign.cpp -o dcm2niix64 g++ -O3 -I. main_console.cpp nii_dicom.cpp jpg_0XC3.cpp ujpeg.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp nii_foreign.cpp -o dcm2niix -DmyDisableOpenJPEG -Wl,-stack_size -Wl,3f00000 lipo -create dcm2niix32 dcm2niix64 -o dcm2niix ``` diff --git a/Canon/README.md b/Canon/README.md index 945ef84c..6a9186a5 100644 --- a/Canon/README.md +++ b/Canon/README.md @@ -37,8 +37,7 @@ In contrast, Canon software [V6.1](https://github.com/neurolabusc/dcm_qa_canon_6 The [BIDS format](https://bids.neuroimaging.io) can record several sequence properties that are useful for processing MRI data. The DICOM headers created by Toshiba scanners are very clean and minimalistic, and do not report several of these advanced properties. Therefore, dcm2niix is unable to populate these properties of the JSON file. This reflects a limitation of the DICOM images, not of dcm2niix. - SliceTiming is not recorded. This can be useful for slice time correction. - - Phase encoding polarity is not record. This is useful for undistortion with [TOPUP](https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/topup). - + - Phase encoding polarity is not recorded. This is useful for undistortion with [TOPUP](https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/topup). Clément Debacker notes that this parameter can be encoded into a XML field called `` inserted in the private tag 700D,1119. However, this requires the user to explicitly request these details and uses XML rather than DICOM so it is not easily parsed. To enable this, you must have the option "Private tag for MRI image" activated on the scanner. You can ask your local clinical scientist/application specialist to enable it. ## Sample Datasets diff --git a/GE/README.md b/GE/README.md index 0ac92e96..7efbdc92 100644 --- a/GE/README.md +++ b/GE/README.md @@ -28,7 +28,7 @@ Knowing the relative timing of the acquisition for each 2D slice in a 3D volume [Some sequences](https://afni.nimh.nih.gov/afni/community/board/read.php?1,154006) encode the RTIA Timer (0021,105E) element. For example, [this DV24 dataset](https://github.com/nikadon/cc-dcm2bids-wrapper/tree/master/dicom-qa-examples/ge-mr750-slice-timing) includes timing data, while [this DV26 dataset does not](https://github.com/neurolabusc/dcm_qa_nih). Be aware that different versions of GE software appear to use different units for 0021,105E. The DV24 example is reported in seconds, while [14.0 uses 1/10000 seconds](https://github.com/rordenlab/dcm2niix/issues/286). An example of the latter format can be found [here](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Archival_MRI). Even with the sequences that do encode the RTIA Timer, there is some debate regarding the accuracy of this element. In the example listed, the slice times are clearly wrong in the first volume. Therefore, dcm2niix always estimates slice times based on the 2nd volume in a time series. -In general, fMRI acquired using GE product sequence (PSD) “epi” with the multiphase option will store slice timing in the Trigger Time (DICOM 0018,1060) element. In contrast, the popular PSD “epiRT” (BrainWave RT, fMRI/DTI package provided by Medical Numerics) does not save this tag (though in some cases it saves the RTIA Timer). Examples are [available](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Slice_timing_correction) for both the “epiRT” and “epi” sequences. +In general, fMRI acquired using GE product sequence (PSD) “epi” with the multiphase option will store slice timing in the Trigger Time (DICOM 0018,1060) element. In contrast, the popular PSD “epiRT” (BrainWave RT, fMRI/DTI package provided by Medical Numerics) does not save this tag (though in some cases it saves the RTIA Timer). Examples are [available](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Slice_timing_correction) for both the “epiRT” and “epi” sequences. The [“epi2” is a special case](https://github.com/rordenlab/dcm2niix/issues/635). The “epiRT” sequences also allow the user to specify the `Group Delay`, which is 0 msec by default. Increasing this value will create a pause at the end of each volume, and this value is recorded in the DICOM header(as 0043,107C, reported in seconds). This option can be used for sparse designs, where one wants a pause after each value. Be aware that the `RepetitionTime` (0018,0080) reported in this header omits the group delay. So a study with a TR of 2000ms and a Group Delay of 55ms will report the values (0018,0080 = 2000, 0043,107C = 0.055), while the actual sampling rate will be 2055ms. This is unintuitive, the TR with respect to tissue contrast is 2055ms, not the reported 2000. @@ -86,7 +86,7 @@ While GE DICOMs will report if the image uses partial Fourier, it will not revea ## Phase-Encoding Polarity -All EPI scans have spatial distortion, particularly those with longer readout times. Tools like [FSL topup](https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/topup/TopupUsersGuide) can leverage data where two spin-echo images are acquired that are identical except for using opposite phase-encoding polarity (e.g. one uses A>P, the other P>A). Each image is distorted with the same magnitude, but in the opposite direction. GE's Rx27 software version and later populate the [Rectilinear Phase Encode Reordering (0018,9034)](http://dicomlookup.com/lookup.asp?sw=Tnumber&q=(0018,9034)) tag which for EPI is set to either LINEAR or REVERSE_LINEAR. +All EPI scans have spatial distortion, particularly those with longer readout times. Tools like [FSL topup](https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/topup/TopupUsersGuide) can leverage data where two spin-echo images are acquired that are identical except for using opposite phase-encoding polarity (e.g. one uses A>P, the other P>A). Each image is distorted with the same magnitude, but in the opposite direction. GE's Rx27 software version and later populate the [Rectilinear Phase Encode Reordering (0018,9034)](http://dicomlookup.com/lookup.asp?sw=Tnumber&q=(0018,9034)) tag which for EPI is set to either LINEAR or REVERSE_LINEAR. This should be given precedence to similar values in [User Define Data GE (0043,102A)](https://github.com/rordenlab/dcm2niix/issues/674). ## GE Protocol Data Block diff --git a/Philips/README.md b/Philips/README.md index 3ac7e353..3c2f81ab 100644 --- a/Philips/README.md +++ b/Philips/README.md @@ -136,7 +136,7 @@ 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. +Important tags for Philips DICOM include b-value index (2005,1412) and gradient direction number (2005,1413). Knowing both 2005,1412 and 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 @@ -152,7 +152,7 @@ Philips DICOMs do not allow one to determine the temporal order of volumes for d 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 [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. +Another value desirable for susceptibility distortion correction (e.g., TOPUP) is the "TotalReadoutTime". Be aware that the [FSL definition](https://github.com/rordenlab/dcm2niix/issues/130) is not intuitive for scans with parallel imaging, phase oversampling, partial Fourier, interpolation, etc. It has been challenging to establish and validate the correct equations to calculate "TotalReadoutTime" on Philips in the presence of all these possible factors, and the issue is further complicated by the fact that Philips have may changed the manner in which it reports the [relevant information over time](https://github.com/rordenlab/dcm2niix/issues/377#issuecomment-598665157). For this reason, for Philips data, `dcm2niix` currently reports `EstimatedTotalReadoutTime` and `EstimatedEffectiveEchoSpacing` (note the inclusion of `Estimated` as part of those variable names). See [issue 377](https://github.com/rordenlab/dcm2niix/issues/377) for a fuller discussion. However, it is also relevant to note that getting "TotalReadoutTime" correct is not critical to the TOPUP correction _if the readout time is identical for all the relevant images_ (i.e., the spin-echo EPI images to estimate the field, as well as any subsequent images being corrected). If that is not the case, or if you want TOPUP to return a valid estimate of the field itself (in Hz; `--fout` flag), then getting "Total Readout Time" correct is important. ## Partial Volumes diff --git a/README.md b/README.md index 39f46ffe..1a9f26ff 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -[![Build Status](https://travis-ci.org/rordenlab/dcm2niix.svg?branch=master)](https://travis-ci.org/rordenlab/dcm2niix) [![Build status](https://ci.appveyor.com/api/projects/status/7o0xp2fgbhadkgn1?svg=true)](https://ci.appveyor.com/project/neurolabusc/dcm2niix) ## About @@ -44,14 +43,15 @@ Command line usage is described in the [NITRC wiki](https://www.nitrc.org/plugin There are a couple ways to install dcm2niix - [Github Releases](https://github.com/rordenlab/dcm2niix/releases) provides the latest compiled executables. This is an excellent option for MacOS and Windows users. However, the provided Linux executable requires a recent version of Linux (e.g. Ubuntu 14.04 or later), so the provided Unix executable is not suitable for very old distributions. Specifically, it requires Glibc 2.19 (from 2014) or later. Users of older systems can compile their own copy of dcm2niix or download the compiled version included with MRIcroGL Glibc 2.12 (from 2011, see below). - - Run the following command to get the latest version for Linux, Macintosh or Windows: + - Run the following command to get the latest release version for Linux, Macintosh or Windows: * `curl -fLO https://github.com/rordenlab/dcm2niix/releases/latest/download/dcm2niix_lnx.zip` - * `curl -fLO https://github.com/rordenlab/dcm2niix/releases/latest/download/dcm2niix_mac.zip` - * `curl -fLO https://github.com/rordenlab/dcm2niix/releases/latest/download/dcm2niix_mac_arm.pkg` + * `curl -fLO https://github.com/rordenlab/dcm2niix/releases/latest/download/macos_dcm2niix.pkg` * `curl -fLO https://github.com/rordenlab/dcm2niix/releases/latest/download/dcm2niix_win.zip` + - Latest development version is available on [AppVeyor](https://ci.appveyor.com/project/neurolabusc/dcm2niix) for [Linux](https://ci.appveyor.com/api/projects/neurolabusc/dcm2niix/artifacts/dcm2niix_lnx.zip?job=linux), [Macintosh](https://ci.appveyor.com/api/projects/neurolabusc/dcm2niix/artifacts/dcm2niix_mac.zip?job=mac) or [Windows](https://ci.appveyor.com/api/projects/neurolabusc/dcm2niix/artifacts/dcm2niix_win.zip?job=win). - [MRIcroGL (NITRC)](https://www.nitrc.org/projects/mricrogl) or [MRIcroGL (GitHub)](https://github.com/rordenlab/MRIcroGL12/releases) includes dcm2niix that can be run from the command line or from the graphical user interface (select the Import menu item). The Linux version of dcm2niix is compiled on a [holy build box](https://github.com/phusion/holy-build-box), so it should run on any Linux distribution. - If you have a MacOS computer with Homebrew or MacPorts you can run `brew install dcm2niix` or `sudo port install dcm2niix`, respectively. - If you have Conda, [`conda install -c conda-forge dcm2niix`](https://anaconda.org/conda-forge/dcm2niix) on Linux, MacOS or Windows. + - If you have pip, `python -m pip install dcm2niix` on Linux, MacOS or Windows. - On Debian Linux computers you can run `sudo apt-get install dcm2niix`. @@ -67,38 +67,30 @@ Ubuntu: `sudo apt-get install cmake pkg-config` MacOS: `brew install cmake pkg-config` or `sudo port install cmake pkgconfig` -**Basic build:** +Once these tools are available, you can compile with cmake: + ```bash git clone https://github.com/rordenlab/dcm2niix.git cd dcm2niix mkdir build && cd build -cmake .. +cmake -DZLIB_IMPLEMENTATION=Cloudflare -DUSE_JPEGLS=ON -DUSE_OPENJPEG=ON .. make ``` `dcm2niix` will be created in the `bin` subfolder. To install on the system run `make install` instead of `make` - this will copy the executable to your path so you do not have to provide the full path to the executable. In rare case if cmake fails with the message like `"Generator: execution of make failed"`, it could be fixed by ``sudo ln -s `which make` /usr/bin/gmake``. -**Advanced build:** +### Building the command line version without cmake -As noted in the `Image Conversion and Compression Support` section, the software provides many optional modules with enhanced features. A common choice might be to include support for JPEG2000, [JPEG-LS](https://github.com/team-charls/charls) (this option requires a c++14 compiler), as well as using the high performance Cloudflare zlib library (this option requires a CPU built after 2008). To build with these options simply request them when configuring cmake: +This is the simplest way to compile dcm2niix on a Linux or MacOS computer. Be warned that this minimal version will not be able to extract DICOM images compressed with the (rarely used) JPEG2000 or JPEG-LS formats. ```bash git clone https://github.com/rordenlab/dcm2niix.git -cd dcm2niix -mkdir build && cd build -cmake -DZLIB_IMPLEMENTATION=Cloudflare -DUSE_JPEGLS=ON -DUSE_OPENJPEG=ON .. +cd dcm2niix/console make +./dcm2niix ``` -**optional batch processing version:** - -The batch processing binary `dcm2niibatch` is optional. To build `dcm2niibatch` as well change the cmake command to `cmake -DBATCH_VERSION=ON ..`. This requires a compiler that supports c++11. - -### Building the command line version without cmake - -If you have any problems with the cmake build script described above or want to customize the software see the [COMPILE.md file for details on manual compilation](./COMPILE.md). - ## Referencing - Li X, Morgan PS, Ashburner J, Smith J, Rorden C (2016) The first step for neuroimaging data analysis: DICOM to NIfTI conversion. J Neurosci Methods. 264:47-56. doi: 10.1016/j.jneumeth.2016.03.001. [PMID: 26945974](https://www.ncbi.nlm.nih.gov/pubmed/26945974) @@ -112,14 +104,14 @@ If you have any problems with the cmake build script described above or want to - [dicom2nifti](https://github.com/icometrix/dicom2nifti) uses the scriptable Python wrapper utilizes the [high performance GDCMCONV](http://gdcm.sourceforge.net/wiki/index.php/Gdcmconv) executables. - [dicomtonifti](https://github.com/dgobbi/vtk-dicom/wiki/dicomtonifti) leverages [VTK](https://www.vtk.org/). - [dimon](https://afni.nimh.nih.gov/pub/dist/doc/program_help/Dimon.html) and [to3d](https://afni.nimh.nih.gov/pub/dist/doc/program_help/to3d.html) are included with AFNI. - - [dinifti](http://as.nyu.edu/cbi/resources/Software/DINIfTI.html) is focused on conversion of Siemens data. + - [dinifti](https://as.nyu.edu/cbi/resources/Software/DINIfTI.html) is focused on conversion of classic Siemens DICOMs. - [DWIConvert](https://github.com/BRAINSia/BRAINSTools/tree/master/DWIConvert) converts DICOM images to NRRD and NIfTI formats. - - [mcverter](http://lcni.uoregon.edu/%7Ejolinda/MRIConvert/) has great support for various vendors. + - [mcverter](http://lcni.uoregon.edu/%7Ejolinda/MRIConvert/) a great tool for classic DICOMs. - [mri_convert](https://surfer.nmr.mgh.harvard.edu/pub/docs/html/mri_convert.help.xml.html) is part of the popular FreeSurfer package. In my limited experience this tool works well for GE and Siemens data, but fails with Philips 4D datasets. - [MRtrix mrconvert](http://mrtrix.readthedocs.io/en/latest/reference/commands/mrconvert.html) is a useful general purpose image converter and handles DTI data well. It is an outstanding tool for modern Philips enhanced images. - [nanconvert](https://github.com/spinicist/nanconvert) uses the ITK library to convert DICOM from GE and proprietary Bruker to standard formats like DICOM. - - [PET CT viewer](http://petctviewer.org/index.php/feature/results-exports/nifti-export) for [Fiji](https://fiji.sc) can load DICOM images and export as NIfTI. - - [Plastimatch](https://www.plastimatch.org/) is a Swiss Army knife - it computes registration, image processing, statistics and it has a basic image format converter that can convert some DICOM images to NIfTI or NRRD. + - [Plastimatch](https://plastimatch.org/) is a Swiss Army knife - it computes registration, image processing, + statistics and it has a basic image format converter that can convert some DICOM images to NIfTI or NRRD. - [Simple Dicom Reader 2 (Sdr2)](http://ogles.sourceforge.net/sdr2-doc/index.html) uses [dcmtk](https://dicom.offis.de/dcmtk.php.en) to read DICOM images and convert them to the NIfTI format. - [SlicerHeart extension](https://github.com/SlicerHeart/SlicerHeart) is specifically designed to help 3D Slicer support ultra sound (US) images stored as DICOM. - [spec2nii](https://github.com/wexeee/spec2nii) converts MR spectroscopy to NIFTI. @@ -141,14 +133,20 @@ The following tools exploit dcm2niix - [bidskit](https://github.com/jmtyszka/bidskit) uses dcm2niix to create [BIDS](http://bids.neuroimaging.io/) datasets. - [BioImage Suite Web Project](https://github.com/bioimagesuiteweb/bisweb) is a JavaScript project that uses dcm2niix for its DICOM conversion module. - [birc-bids](https://github.com/bircibrain/birc-bids) provides a Docker/Singularity container with various BIDS conversion utilities. + - [BMAT](https://github.com/ColinVDB/BMAT) translates data from MRI scanners to the BIDS structure. - [BOLD5000_autoencoder](https://github.com/nmningmei/BOLD5000_autoencoder) uses dcm2niix to pipe imaging data into an unsupervised machine learning algorithm. - [boutiques-dcm2niix](https://github.com/lalet/boutiques-dcm2niix) is a dockerfile for installing and validating dcm2niix. - [Brain imAgiNg Analysis iN Arcana (Banana)](https://pypi.org/project/banana/) is a collection of brain imaging analysis workflows, it uses dcm2niix for format conversions. - [brainnetome DiffusionKit](http://diffusion.brainnetome.org/en/latest/) uses dcm2niix to convert images. - [BraTS-Preprocessor](https://neuronflow.github.io/BraTS-Preprocessor/) uses dcm2niix to import files for [Brain Tumor Segmentation](https://www.frontiersin.org/articles/10.3389/fnins.2020.00125/full). + - [CardioNIfTI](https://github.com/UK-Digital-Heart-Project/CardioNIfTI) processes cardiac MR DICOM datasets and converts them to NIfTI. - [clinica](https://github.com/aramis-lab/clinica) is a software platform for clinical neuroimaging studies that uses dcm2niix to convert DICOM images. + - [clinical_dicom2bids_smk](https://github.com/greydongilmore/clinical_dicom2bids_smk) Snakemake workflow to convert a clinical dicom directory into BIDS structure. - [clpipe](https://github.com/cohenlabUNC/clpipe) uses dcm2bids for DICOM import. - [conversion](https://github.com/pnlbwh/conversion) is a Python library that can convert dcm2niix created NIfTI files to the popular NRRD format (including DWI gradient tables). Note, recent versions of dcm2niix can directly convert DICOM images to NRRD. + - [convert_source](https://github.com/AdebayoBraimah/convert_source) to convert DICOM to BIDS directory layout. + - [CT-preprocess](https://github.com/GravO8/CT-preprocess) brain extract head CT scans. + - [d2b-dcm2niix](https://github.com/d2b-dev/d2b-dcm2niix) data to BIDS wrapper. - [DAC2BIDS](https://github.com/dangom/dac2bids) uses dcm2niibatch to create [BIDS](http://bids.neuroimaging.io/) datasets. - [Data2Bids](https://github.com/SIMEXP/Data2Bids) converts non-DICOM images with associated JSON files to BIDS. While this tool does not require dcm2niix, it can leverage dcm2niix output similar to niix2bids. - [Dcm2Bids](https://github.com/cbedetti/Dcm2Bids) uses dcm2niix to create [BIDS](http://bids.neuroimaging.io/) datasets. Here is a [tutorial](https://andysbrainbook.readthedocs.io/en/latest/OpenScience/OS/BIDS_Overview.html) describing usage. @@ -162,23 +160,28 @@ The following tools exploit dcm2niix - [dicom2bids](https://github.com/Jolinda/lcnimodules) includes python modules for converting dicom files to nifti in a bids-compatible file structure that use dcm2niix. - [DICOM2BIDS](https://github.com/klsea/DICOM2BIDS) is a Python 2 script for creating BIDS files. - [dicom2nifti_batch](https://github.com/scanUCLA/dicom2nifti_batch) is a Matlab script for automating dcm2niix. + - [dicomConversionToNifti](https://github.com/bsmarine/dicomConversionToNifti) converts, de-identifies and assigns standardized naming convention to medical imaging. - [divest](https://github.com/jonclayden/divest) R interface to dcm2niix. + - [DPABI Data Processing & Analysis for Brain Imaging](http://rfmri.org/dpabi) includes dcm2niix. - [ExploreASL](https://sites.google.com/view/exploreasl/exploreasl) uses dcm2niix to import images. + - [ExploreASL-GUI](https://github.com/MauricePasternak/ExploreASL-GUI) uses dcm2niix for image conversion. - [ezBIDS](https://github.com/brainlife/ezbids) is a [web service](https://brainlife.io/ezbids/) for converting directory full of DICOM images into BIDS without users having to learn python nor custom configuration file. - [fmrif tools](https://github.com/nih-fmrif/fmrif_tools) uses dcm2niix for its [oxy2bids](https://fmrif-tools.readthedocs.io/en/latest/#) tool. - [fMRIprep.dcm2niix](https://github.com/BrettNordin/fMRIprep.dcm2niix) is designed to convert DICOM format to the NIfTI format. - [FreeSurfer](https://github.com/freesurfer/freesurfer) includes dcm2niix for image conversion. - [fsleyes](https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/FSLeyes) is a powerful Python-based image viewer. It uses dcm2niix to handle DICOM files through its fslpy libraries. - [Functional Real-Time Interactive Endogenous Neuromodulation and Decoding (FRIEND) Engine](https://github.com/InstitutoDOr/FriendENGINE) uses dcm2niix. + - [https://github.com/TamerGezici/HCF-bidser](https://github.com/TamerGezici/HCF-bidser) Jupyter notebook script for DICOM to BIDS format. - [heudiconv](https://github.com/nipy/heudiconv) can use dcm2niix to create [BIDS](http://bids.neuroimaging.io/) datasets. Data acquired using the [reproin](https://github.com/ReproNim/reproin) convention can be easily converted to BIDS. - [Horos (Osirix) Bids Output Extension](https://github.com/mslw/horos-bids-output) is a OsiriX / Horos plugin that uses dcm2niix for creating BIDS output. - [kipettools](https://github.com/mathesong/kipettools) uses dcm2niix to load PET data. - [LEAD-DBS](http://www.lead-dbs.org/) uses dcm2niix for [DICOM import](https://github.com/leaddbs/leaddbs/blob/master/ea_dicom_import.m). - [lin4neuro](http://www.lin4neuro.net/lin4neuro/18.04bionic/vm/) releases such as the English l4n-18.04.4-amd64-20200801-en.ova include MRIcroGL and dcm2niix pre-installed. This allows user with VirtualBox or VMWarePlayer to use these tools (and many other neuroimaging tools) in a graphical virtual machine. - [MRIcroGL](https://github.com/neurolabusc/MRIcroGL) is available for MacOS, Linux and Windows and provides a graphical interface for dcm2niix. You can get compiled copies from the [MRIcroGL NITRC web site](https://www.nitrc.org/projects/mricrogl/). + - [MrPyConvert](https://github.com/Jolinda/mrpyconvert) Python library dicom to bids conversion. + - [Nekton](https://github.com/deepc-health/nekton) is a python package for DICOM to NifTi and NifTi to DICOM-SEG and GSPS conversion. - [neuro_docker](https://github.com/Neurita/neuro_docker) includes dcm2niix as part of a single, static Dockerfile. - [NeuroDebian](http://neuro.debian.net/pkgs/dcm2niix.html) provides up-to-date version of dcm2niix for Debian-based systems. - - [neurodocker](https://github.com/kaczmarj/neurodocker) generates [custom](https://github.com/rordenlab/dcm2niix/issues/138) Dockerfiles given specific versions of neuroimaging software. - [neurodocker](https://github.com/kaczmarj/neurodocker) includes dcm2niix as a lean, minimal install Dockerfile. - [NeuroElf](http://neuroelf.net) can use dcm2niix to convert DICOM images. - [Neuroinformatics Database (NiDB)](https://github.com/gbook/nidb) is designed to store, retrieve, analyze, and share neuroimaging data. It uses dcm2niix for image QA and handling some formats. @@ -186,9 +189,11 @@ The following tools exploit dcm2niix - [niix2bids](https://github.com/benoitberanger/niix2bids ) attempts to automatically convert Siemens MRI images converted by dcm2niix to BIDS. - [nipype](https://github.com/nipy/nipype) can use dcm2niix to convert images. - [PET2BIDS](https://github.com/openneuropet/PET2BIDS) uses dcm2niix for DICOM images. + - [pl-dcm2niix](https://github.com/FNNDSC/pl-dcm2niix) is a ChRIS wrapper for dcm2niix. - [py2bids](https://github.com/Jolinda/py2bids) dcm2niix dicom to bids conversion wrapper. - [pyBIDSconv provides a graphical format for converting DICOM images to the BIDS format](https://github.com/DrMichaelLindner/pyBIDSconv). It includes clever default heuristics for identifying Siemens scans. - [pydcm2niix is a Python module for working with dcm2niix](https://github.com/jstutters/pydcm2niix). + - [pydra-dcm2bids](https://github.com/aramis-lab/pydra-dcm2bids) supports Pydra tasks for dcm2bids. - [pydra-dcm2niix](https://github.com/nipype/pydra-dcm2niix) is a contains Pydra task interface for dcm2niix. - [qsm](https://github.com/CAIsr/qsm) Quantitative Susceptibility Mapping software. - [reproin](https://github.com/ReproNim/reproin) is a setup for automatic generation of shareable, version-controlled BIDS datasets from MR scanners. @@ -199,4 +204,5 @@ The following tools exploit dcm2niix - [tar2bids](https://github.com/khanlab/tar2bids) converts DICOM tarball(s) to BIDS using heudiconv which invokes dcm2niix. - [TORTOISE](https://tortoise.nibib.nih.gov) is used for processing diffusion MRI data, and uses dcm2niix to import DICOM images. - [TractoR (Tracto­graphy with R) uses dcm2niix for image conversion](http://www.tractor-mri.org.uk/TractoR-and-DICOM). + - [twice_exceptionality_repository](https://github.com/avery-water/twice_exceptionality_repository) converts DICOM to BIDS format, creates masks, and runs VBM. - [XNAT2BIDS](https://github.com/kamillipi/2bids) is a simple xnat pipeline to convert DICOM scans to BIDS-compatible output. diff --git a/Siemens/README.md b/Siemens/README.md index d387031e..3e883205 100644 --- a/Siemens/README.md +++ b/Siemens/README.md @@ -6,7 +6,7 @@ dcm2niix attempts to convert Siemens DICOM format images to NIfTI. This page des Siemens MR is named by Series, Generation, Major Version and Minor Version. Prior to the Siemens Vida, all contemporary Siemens MRI systems (Trio, Prisma, Skyra, etc) were part of the V series. So a Trio might be on VB17, and a Prisma on VE11 (series 'V', generation 'E', major version '1', minor version '1'). The 3T Vida and 1.5T Sola introduce the X-series (XA10, XA11, XA20). Since the V-series was dominant for so long, most users simply omit the series, e.g. referring to a system as `B19`. However, Siemens has recently introduced a new X-series. -The DICOM images exported by the X-series is radically different than the V-series. The images lack the proprietary CSA header with its rich meta data. +The DICOM images exported by the X-series is [radically different](https://wikis.utexas.edu/display/IRC/New+Enhanced+DICOM+format) than the V-series. The images lack the proprietary CSA header with its rich meta data. 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. @@ -38,6 +38,7 @@ The private `ICE_Dims` (0021,1106) tag can prove useful for parsing data. The li (0021,1106) LO [X_4_1_1_1_1_160_1_1_1_1_1_277] # ICE_Dims ``` +0. coi = [coil number](https://github.com/rordenlab/dcm2niix/issues/631) (X: combined from multiple coils) 1. eco = echo number 2. phs = phase encode 3. set = diff --git a/SuperBuild/External-CLOUDFLARE-ZLIB.cmake b/SuperBuild/External-CLOUDFLARE-ZLIB.cmake index 9f064ebc..4c6a6acb 100644 --- a/SuperBuild/External-CLOUDFLARE-ZLIB.cmake +++ b/SuperBuild/External-CLOUDFLARE-ZLIB.cmake @@ -1,13 +1,18 @@ set(CLOUDFLARE_BRANCH gcc.amd64) # Cloudflare zlib branch ExternalProject_Add(zlib - GIT_REPOSITORY "${git_protocol}://github.com/ningfei/zlib.git" + GIT_REPOSITORY "https://github.com/ningfei/zlib.git" GIT_TAG "${CLOUDFLARE_BRANCH}" SOURCE_DIR cloudflare-zlib BINARY_DIR cloudflare-zlib-build CMAKE_ARGS -Wno-dev - -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} + ${EXTERNAL_PROJECT_BUILD_TYPE_CMAKE_ARGS} + ${OSX_ARCHITECTURES} + # Compiler settings + -DCMAKE_C_COMPILER:FILEPATH=${CMAKE_C_COMPILER} + -DCMAKE_CXX_COMPILER:FILEPATH=${CMAKE_CXX_COMPILER} + # Install directories -DCMAKE_INSTALL_PREFIX:PATH=${DEP_INSTALL_DIR} ) diff --git a/SuperBuild/External-OPENJPEG.cmake b/SuperBuild/External-OPENJPEG.cmake index 565c14cb..1d99ac28 100644 --- a/SuperBuild/External-OPENJPEG.cmake +++ b/SuperBuild/External-OPENJPEG.cmake @@ -1,14 +1,19 @@ set(OPENJPEG_TAG v2.1-static) # version v2.1-static ExternalProject_Add(openjpeg - GIT_REPOSITORY "${git_protocol}://github.com/ningfei/openjpeg.git" + GIT_REPOSITORY "https://github.com/ningfei/openjpeg.git" GIT_TAG "${OPENJPEG_TAG}" SOURCE_DIR openjpeg BINARY_DIR openjpeg-build CMAKE_ARGS -Wno-dev --no-warn-unused-cli - -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} + ${EXTERNAL_PROJECT_BUILD_TYPE_CMAKE_ARGS} + ${OSX_ARCHITECTURES} + # Compiler settings + -DCMAKE_C_COMPILER:FILEPATH=${CMAKE_C_COMPILER} + # Not used -DCMAKE_CXX_COMPILER:FILEPATH=${CMAKE_CXX_COMPILER} + # Install directories -DCMAKE_INSTALL_PREFIX:PATH=${DEP_INSTALL_DIR} ) diff --git a/SuperBuild/External-YAML-CPP.cmake b/SuperBuild/External-YAML-CPP.cmake index 2e1aef1e..ed85ca76 100644 --- a/SuperBuild/External-YAML-CPP.cmake +++ b/SuperBuild/External-YAML-CPP.cmake @@ -1,14 +1,19 @@ set(YAML-CPP_TAG yaml-cpp-0.5.3) # version yaml-cpp-0.5.3 ExternalProject_Add(yaml-cpp - GIT_REPOSITORY "${git_protocol}://github.com/ningfei/yaml-cpp.git" + GIT_REPOSITORY "https://github.com/ningfei/yaml-cpp.git" GIT_TAG "${YAML-CPP_TAG}" SOURCE_DIR yaml-cpp BINARY_DIR yaml-cpp-build CMAKE_ARGS -Wno-dev --no-warn-unused-cli - -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} + ${EXTERNAL_PROJECT_BUILD_TYPE_CMAKE_ARGS} + ${OSX_ARCHITECTURES} + # Compiler settings + -DCMAKE_C_COMPILER:FILEPATH=${CMAKE_C_COMPILER} + -DCMAKE_CXX_COMPILER:FILEPATH=${CMAKE_CXX_COMPILER} + # Install directories -DCMAKE_INSTALL_PREFIX:PATH=${DEP_INSTALL_DIR} ) diff --git a/SuperBuild/SuperBuild.cmake b/SuperBuild/SuperBuild.cmake index e1057a0f..98f20c6c 100644 --- a/SuperBuild/SuperBuild.cmake +++ b/SuperBuild/SuperBuild.cmake @@ -4,20 +4,8 @@ if(NOT GIT_FOUND) message(FATAL_ERROR "Cannot find Git. Git is required for Superbuild") endif() -# Use git protocol or not -option(USE_GIT_PROTOCOL "If behind a firewall turn this off to use http instead." OFF) -if(USE_GIT_PROTOCOL) - set(git_protocol "git") -else() - set(git_protocol "https") -endif() +include(${CMAKE_SOURCE_DIR}/cmake/dcm2niixInitializeBuildType.cmake) -# Basic CMake build settings -if(NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE "Release" CACHE STRING - "Choose the type of build, options are: Debug Release RelWithDebInfo MinSizeRel." FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug;Release;RelWithDebInfo;MinSizeRel") -endif() set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) option(USE_STATIC_RUNTIME "Use static runtime" ON) @@ -36,9 +24,14 @@ if(USE_STATIC_RUNTIME) endif() endif() +if(APPLE) + set(OSX_ARCHITECTURES "-DCMAKE_OSX_ARCHITECTURES:STRING=${CMAKE_OSX_ARCHITECTURES}") +endif() + option(USE_TURBOJPEG "Use TurboJPEG to decode classic JPEG" OFF) option(USE_JASPER "Build with JPEG2000 support using Jasper" OFF) -option(USE_OPENJPEG "Build with JPEG2000 support using OpenJPEG" OFF) +set(USE_OPENJPEG "OFF" CACHE STRING "Build with JPEG2000 support using OpenJPEG.") +set_property(CACHE USE_OPENJPEG PROPERTY STRINGS "OFF;GitHub;System;Custom") option(USE_JPEGLS "Build with JPEG-LS support using CharLS" OFF) option(USE_JNIFTI "Build with JNIFTI support" ON) @@ -67,56 +60,68 @@ else() set(DEP_INSTALL_DIR ${CMAKE_BINARY_DIR}) endif() -if(USE_OPENJPEG) +if(NOT ${USE_OPENJPEG} STREQUAL "OFF") message("-- Build with OpenJPEG: ${USE_OPENJPEG}") if(OpenJPEG_DIR) + set(USE_OPENJPEG "Custom" CACHE STRING "Build with JPEG2000 support using OpenJPEG." FORCE) set(OpenJPEG_DIR "${OpenJPEG_DIR}" CACHE PATH "Path to OpenJPEG configuration file" FORCE) message("-- Using OpenJPEG library from ${OpenJPEG_DIR}") - else() + elseif(${USE_OPENJPEG} STREQUAL "System") find_package(PkgConfig) if(PKG_CONFIG_FOUND) pkg_check_modules(OPENJPEG libopenjp2) endif() - if(OPENJPEG_FOUND AND NOT ${OPENJPEG_INCLUDE_DIRS} MATCHES "gdcmopenjpeg") - set(OpenJPEG_DIR ${OPENJPEG_LIBDIR}/openjpeg-${OPENJPEG_VERSION} CACHE PATH "Path to OpenJPEG configuration file" FORCE) - message("-- Using OpenJPEG library from ${OpenJPEG_DIR}") - else() - if(${OPENJPEG_INCLUDE_DIRS} MATCHES "gdcmopenjpeg") + if(OPENJPEG_FOUND) + if(NOT ${OPENJPEG_INCLUDE_DIRS} MATCHES "gdcmopenjpeg") + set(OpenJPEG_DIR ${OPENJPEG_LIBDIR}/openjpeg-${OPENJPEG_VERSION} CACHE PATH "Path to OpenJPEG configuration file" FORCE) + message("-- Using OpenJPEG library from ${OpenJPEG_DIR}") + else() message("-- Unable to use GDCM's internal OpenJPEG") endif() - include(${CMAKE_SOURCE_DIR}/SuperBuild/External-OPENJPEG.cmake) - list(APPEND DEPENDENCIES openjpeg) - set(BUILD_OPENJPEG TRUE) - message("-- Will build OpenJPEG library from github") endif() endif() + + if(${USE_OPENJPEG} STREQUAL "GitHub" OR NOT OpenJPEG_DIR) + set(USE_OPENJPEG "GitHub" CACHE STRING "Build with JPEG2000 support using OpenJPEG." FORCE) + include(${CMAKE_SOURCE_DIR}/SuperBuild/External-OPENJPEG.cmake) + list(APPEND DEPENDENCIES openjpeg) + set(BUILD_OPENJPEG TRUE) + message("-- Will build OpenJPEG library from GitHub") + endif() endif() if(BATCH_VERSION) message("-- Build dcm2niibatch: ${BATCH_VERSION}") + set(YAML-CPP_IMPLEMENTATION "GitHub" CACHE STRING "Choose yaml-cpp implementation.") + set_property(CACHE YAML-CPP_IMPLEMENTATION PROPERTY STRINGS "GitHub;System;Custom") + if(YAML-CPP_DIR) + set(YAML-CPP_IMPLEMENTATION "Custom" CACHE STRING "Choose yaml-cpp implementation." FORCE) set(YAML-CPP_DIR ${YAML-CPP_DIR} CACHE PATH "Path to yaml-cpp configuration file" FORCE) message("-- Using yaml-cpp library from ${YAML-CPP_DIR}") - else() + elseif(${YAML-CPP_IMPLEMENTATION} STREQUAL "System") find_package(PkgConfig) if(PKG_CONFIG_FOUND) pkg_check_modules(YAML-CPP yaml-cpp) endif() - # Build from github if not found or version < 0.5.3 + # Build from GitHub if not found or version < 0.5.3 if(YAML-CPP_FOUND AND NOT (YAML-CPP_VERSION VERSION_LESS "0.5.3")) set(YAML-CPP_DIR ${YAML-CPP_LIBDIR}/cmake/yaml-cpp CACHE PATH "Path to yaml-cpp configuration file" FORCE) message("-- Using yaml-cpp library from ${YAML-CPP_DIR}") - else() - include(${CMAKE_SOURCE_DIR}/SuperBuild/External-YAML-CPP.cmake) - list(APPEND DEPENDENCIES yaml-cpp) - set(BUILD_YAML-CPP TRUE) - message("-- Will build yaml-cpp library from github") endif() endif() + + if(${YAML-CPP_IMPLEMENTATION} STREQUAL "GitHub" OR NOT YAML-CPP_DIR) + set(YAML-CPP_IMPLEMENTATION "GitHub" CACHE STRING "Choose yaml-cpp implementation." FORCE) + include(${CMAKE_SOURCE_DIR}/SuperBuild/External-YAML-CPP.cmake) + list(APPEND DEPENDENCIES yaml-cpp) + set(BUILD_YAML-CPP TRUE) + message("-- Will build yaml-cpp library from GitHub") + endif() endif() set(ZLIB_IMPLEMENTATION "Miniz" CACHE STRING "Choose zlib implementation.") @@ -126,7 +131,7 @@ if(${ZLIB_IMPLEMENTATION} STREQUAL "Cloudflare") include(${CMAKE_SOURCE_DIR}/SuperBuild/External-CLOUDFLARE-ZLIB.cmake) list(APPEND DEPENDENCIES zlib) set(BUILD_CLOUDFLARE-ZLIB TRUE) - message("-- Will build Cloudflare zlib from github") + message("-- Will build Cloudflare zlib from GitHub") elseif(${ZLIB_IMPLEMENTATION} STREQUAL "Custom") set(ZLIB_ROOT ${ZLIB_ROOT} CACHE PATH "Specify custom zlib root directory.") if(NOT ZLIB_ROOT) @@ -142,16 +147,23 @@ ExternalProject_Add(console CMAKE_ARGS -Wno-dev --no-warn-unused-cli - -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} + ${EXTERNAL_PROJECT_BUILD_TYPE_CMAKE_ARGS} + ${OSX_ARCHITECTURES} + # Install directories -DCMAKE_INSTALL_PREFIX:PATH=${CMAKE_BINARY_DIR} + # Compiler settings + -DCMAKE_C_COMPILER:FILEPATH=${CMAKE_C_COMPILER} -DCMAKE_C_FLAGS:STRING=${CMAKE_C_FLAGS} + -DCMAKE_CXX_COMPILER:FILEPATH=${CMAKE_CXX_COMPILER} -DCMAKE_CXX_FLAGS:STRING=${CMAKE_CXX_FLAGS} + # Options -DCMAKE_VERBOSE_MAKEFILE:BOOL=${CMAKE_VERBOSE_MAKEFILE} -DUSE_STATIC_RUNTIME:BOOL=${USE_STATIC_RUNTIME} -DUSE_TURBOJPEG:BOOL=${USE_TURBOJPEG} -DUSE_JASPER:BOOL=${USE_JASPER} -DUSE_JPEGLS:BOOL=${USE_JPEGLS} -DUSE_JNIFTI:BOOL=${USE_JNIFTI} + # ZLIB -DZLIB_IMPLEMENTATION:STRING=${ZLIB_IMPLEMENTATION} -DZLIB_ROOT:PATH=${ZLIB_ROOT} # OpenJPEG @@ -164,8 +176,10 @@ ExternalProject_Add(console -DBUILD_DCM2NIIXFSLIB:BOOL=${BUILD_DCM2NIIXFSLIB} ) -install(DIRECTORY ${CMAKE_BINARY_DIR}/bin/ DESTINATION bin - USE_SOURCE_PERMISSIONS) +if(SKBUILD) + install(DIRECTORY ${CMAKE_BINARY_DIR}/bin/ DESTINATION dcm2niix USE_SOURCE_PERMISSIONS) +endif() +install(DIRECTORY ${CMAKE_BINARY_DIR}/bin/ DESTINATION bin USE_SOURCE_PERMISSIONS) option(BUILD_DOCS "Build documentation (manpages)" OFF) if(BUILD_DOCS) diff --git a/VERSIONS.md b/VERSIONS.md index 0f61dc09..c1f2a22f 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -1,4 +1,8 @@ -## Versions +## Modern Releases + + - [See Github releases for major release notes](https://github.com/rordenlab/dcm2niix/releases). + +## Legacy Releases 14-November-2018 - [GE images provide more BIDS tags](https://github.com/rordenlab/dcm2niix/issues/163). diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 65b1f885..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,62 +0,0 @@ -version: build-{build} - -image: Visual Studio 2015 - -configuration: Release - -platform: x64 - -clone_depth: 1 - -clone_folder: c:\projects\dcm2niix - -init: -- ps: >- - $env:DATE = $(Get-Date -Format d-MMM-yyyy) - - $githash = $env:APPVEYOR_REPO_COMMIT.Substring(0, 7) - - $gittag = if ($env:APPVEYOR_REPO_TAG -eq $True) {"_$($env:APPVEYOR_REPO_TAG_NAME)"} else {""} - - Update-AppveyorBuild -Version "$($env:DATE)_g${githash}${gittag}" - - $env:release_version = $(Get-Date -Format d-MMMM-yyyy) - -before_build: -- cmd: >- - echo "Running cmake" - - mkdir c:\projects\dcm2niix\build - - cd c:\projects\dcm2niix\build - - cmake -Wno-dev -G "Visual Studio 14 2015 Win64" -DBATCH_VERSION=ON -DUSE_OPENJPEG=ON -DUSE_JPEGLS=true ..\ - -build: - project: c:\projects\dcm2niix\build\dcm2niix.sln - - verbosity: normal - -after_build: -- ps: >- - cd c:\projects\dcm2niix - - 7z a dcm2niix_win.zip c:\projects\dcm2niix\build\bin\* >$null - -artifacts: - - path: dcm2niix*.zip - name: dcm2niix - -deploy: - - provider: GitHub - tag: $(appveyor_repo_tag_name) - release: version $(release_version) ($(appveyor_repo_tag_name)) - description: "" - auth_token: - secure: gCltVLQEWsjSTRlsi8qw7FGP54ujBq60apjXkWTV954b65bOHl95hXMxxkQ734L4 - artifact: dcm2niix - draft: false - prerelease: false - on: - branch: master - appveyor_repo_tag: true diff --git a/cmake/dcm2niixInitializeBuildType.cmake b/cmake/dcm2niixInitializeBuildType.cmake new file mode 100644 index 00000000..b21ba4a8 --- /dev/null +++ b/cmake/dcm2niixInitializeBuildType.cmake @@ -0,0 +1,47 @@ +# This module allows to consistently manage the initialization and setting of build type +# CMake variables. +# +# It sets the variable EXTERNAL_PROJECT_BUILD_TYPE_CMAKE_ARGS based on the CMake generator +# being used: +# * If a multi-config generator (e.g Visual Studio) is used, it sets the variable with +# CMAKE_CONFIGURATION_TYPES. +# * If a single-config generator (e.g Unix Makefiles) is used, it sets the variable with +# CMAKE_BUILD_TYPE. +# +# Adapted from https://github.com/Slicer/Slicer/blob/5.2/CMake/SlicerInitializeBuildType.cmake + +# Default build type to use if none was specified +if(NOT DEFINED dcm2niix_DEFAULT_BUILD_TYPE) + set(dcm2niix_DEFAULT_BUILD_TYPE "Release") +endif() + +# Set a default build type if none was specified +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + + message(STATUS "Setting build type to '${dcm2niix_DEFAULT_BUILD_TYPE}' as none was specified.") + + set(CMAKE_BUILD_TYPE ${dcm2niix_DEFAULT_BUILD_TYPE} CACHE STRING "Choose the type of build." FORCE) + mark_as_advanced(CMAKE_BUILD_TYPE) + + # Set the possible values of build type for cmake-gui + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" + "Release" + "MinSizeRel" + "RelWithDebInfo" + ) +endif() + +# Pass variables to dependent projects +if(COMMAND ExternalProject_Add) + if(NOT CMAKE_CONFIGURATION_TYPES) + set(EXTERNAL_PROJECT_BUILD_TYPE_CMAKE_ARGS + -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} + ) + else() + set(EXTERNAL_PROJECT_BUILD_TYPE_CMAKE_ARGS + -DCMAKE_CONFIGURATION_TYPES:STRING=${CMAKE_CONFIGURATION_TYPES} + ) + endif() +endif() + diff --git a/console/CMakeLists.txt b/console/CMakeLists.txt index 0dcfe303..aa52bd7c 100644 --- a/console/CMakeLists.txt +++ b/console/CMakeLists.txt @@ -1,257 +1,262 @@ -cmake_minimum_required(VERSION 2.8.11) - -project(console) - -# Option Choose whether to use static runtime -include(ucm.cmake) -option(USE_STATIC_RUNTIME "Use static runtime" ON) -if(USE_STATIC_RUNTIME) - ucm_set_runtime(STATIC) -else() - ucm_set_runtime(DYNAMIC) -endif() - -# Basic CMake build settings -if(NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE "Release" CACHE STRING - "Choose the type of build, options are: Debug Release RelWithDebInfo MinSizeRel." FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug;Release;RelWithDebInfo;MinSizeRel") -endif() - -if(${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") - # using Clang - add_definitions(-fno-caret-diagnostics) - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-dead_strip") -elseif(${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU") - # using GCC - if(NOT (${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS 7.1.0)) - add_definitions(-Wno-format-overflow) # available since GCC 7.1.0 - endif() - if(NOT (${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS 4.5.0)) - add_definitions(-Wno-unused-result) # available since GCC 4.5.0 - endif() - if(NOT (${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS 4.8.0)) - add_definitions(-fno-diagnostics-show-caret) # available since GCC 4.8.0 - endif() -elseif(MSVC) - # using Visual Studio C++ - add_definitions(-D_CRT_SECURE_NO_DEPRECATE) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4018") # '<': signed/unsigned mismatch - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4068") # unknown pragma - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4101") # unreferenced local variable - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4244") # 'initializing': conversion from 'double' to 'int', possible loss of data - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4267") # 'initializing': conversion from 'size_t' to 'int', possible loss of data - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4305") # 'argument': truncation from 'double' to 'float' - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4308") # negative integral constant converted to unsigned type - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4334") # '<<': result of 32-bit shift implicitly converted to 64 bits - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4800") # 'uint32_t' : forcing value to bool 'true' or 'false' - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4819") # The file contains a character that cannot be represented in the current code page - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4996") # 'access': The POSIX name for this item is deprecated - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /STACK:8388608") # set "Stack Reserve Size" to 8MB (default value is 1MB) -endif() - -# Compiler dependent flags -include (CheckCXXCompilerFlag) -if(UNIX) - check_cxx_compiler_flag(-march=armv8-a+crc ARM_CRC) - if(ARM_CRC) - # wrong answer for Apple Silicon: check_cxx_compiler_flag(-msse2 HAS_SSE2) - else() - check_cxx_compiler_flag(-msse2 HAS_SSE2) - if(HAS_SSE2) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -msse2 -mfpmath=sse") - endif() - endif() -endif() - -set(PROGRAMS dcm2niix) - -option(USE_TURBOJPEG "Use TurboJPEG to decode classic JPEG" OFF) -option(USE_JASPER "Build with JPEG2000 support using Jasper" OFF) -option(USE_OPENJPEG "Build with JPEG2000 support using OpenJPEG" OFF) -option(USE_JPEGLS "Build with JPEG-LS support using CharLS" OFF) - -option(BATCH_VERSION "Build dcm2niibatch for multiple conversions" OFF) - -option(BUILD_DCM2NIIXFSLIB "Build libdcm2niixfs.a" OFF) - -if(USE_OPENJPEG OR USE_TURBOJPEG OR USE_JASPER) - message("-- Set BUILD_DCM2NIIXFSLIB to OFF since USE_TURBOJPEG/USE_JASPER/USE_OPENJPEG is ON.") - set(BUILD_DCM2NIIXFSLIB OFF CACHE BOOL "Build libdcm2niixfs.a" FORCE) -endif() - -set(DCM2NIIX_SRCS - main_console.cpp - nii_dicom.cpp - jpg_0XC3.cpp - ujpeg.cpp - nifti1_io_core.cpp - nii_foreign.cpp - nii_ortho.cpp - nii_dicom_batch.cpp) - - -option(USE_JNIfTI "Build with JNIfTI support" ON) -if(USE_JNIFTI) - add_definitions(-DmyEnableJNIfTI) - set(DCM2NIIX_SRCS ${DCM2NIIX_SRCS} cJSON.cpp base64.cpp) -endif() - -if(BUILD_DCM2NIIXFSLIB) - set(DCM2NIIXFSLIB dcm2niixfs) - set(DCM2NIIXFSLIB_SRCS - dcm2niix_fswrapper.cpp - nii_dicom.cpp - jpg_0XC3.cpp - ujpeg.cpp - nifti1_io_core.cpp - nii_foreign.cpp - nii_ortho.cpp - nii_dicom_batch.cpp) -endif() - -if(USE_JPEGLS) - add_definitions(-DmyEnableJPEGLS) - if(MSVC) - add_definitions(-DCHARLS_STATIC) - endif() - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14") - - set(CHARLS_SRCS - charls/jpegls.cpp - charls/jpegmarkersegment.cpp - charls/interface.cpp - charls/jpegstreamwriter.cpp - charls/jpegstreamreader.cpp) - add_executable(dcm2niix ${DCM2NIIX_SRCS} ${CHARLS_SRCS}) - - if(BUILD_DCM2NIIXFSLIB) - add_library(${DCM2NIIXFSLIB} STATIC ${DCM2NIIXFSLIB_SRCS} ${CHARLS_SRCS}) - endif() -else() - add_executable(dcm2niix ${DCM2NIIX_SRCS}) - - if(BUILD_DCM2NIIXFSLIB) - add_library(${DCM2NIIXFSLIB} STATIC ${DCM2NIIXFSLIB_SRCS}) - endif() -endif() - -set(ZLIB_IMPLEMENTATION "Miniz" CACHE STRING "Choose zlib implementation.") -set_property(CACHE ZLIB_IMPLEMENTATION PROPERTY STRINGS "Miniz;System;Custom") -if(NOT ${ZLIB_IMPLEMENTATION} STREQUAL "Miniz") - if(NOT ${ZLIB_IMPLEMENTATION} STREQUAL "System") - set(ZLIB_ROOT ${ZLIB_ROOT} CACHE PATH "Specify custom zlib root directory.") - if(NOT ZLIB_ROOT) - message(FATAL_ERROR "ZLIB_ROOT needs to be set to locate custom zlib!") - endif() - endif() - find_package(ZLIB REQUIRED) - add_definitions(-DmyDisableMiniZ) - target_include_directories(dcm2niix PRIVATE ${ZLIB_INCLUDE_DIRS}) - target_link_libraries(dcm2niix ${ZLIB_LIBRARIES}) -endif() - -if(USE_TURBOJPEG) - find_package(PkgConfig REQUIRED) - pkg_check_modules(TURBOJPEG REQUIRED libturbojpeg) - add_definitions(-DmyTurboJPEG) - target_include_directories(dcm2niix PRIVATE ${TURBOJPEG_INCLUDEDIR}) - target_link_libraries(dcm2niix ${TURBOJPEG_LIBRARIES}) -endif() - -if(USE_JASPER) - find_package(Jasper REQUIRED) - add_definitions(-DmyEnableJasper) - target_include_directories(dcm2niix PRIVATE ${JASPER_INCLUDE_DIR}) - target_link_libraries(dcm2niix ${JASPER_LIBRARIES}) -endif() - -if(USE_OPENJPEG) - set(OpenJPEG_DIR "${OpenJPEG_DIR}" CACHE PATH "Path to OpenJPEG configuration file" FORCE) - - find_package(OpenJPEG REQUIRED) - - if(WIN32) - if(BUILD_SHARED_LIBS) - add_definitions(-DOPJ_EXPORTS) - else() - add_definitions(-DOPJ_STATIC) - endif() - endif() - - target_include_directories(dcm2niix PRIVATE ${OPENJPEG_INCLUDE_DIRS}) - target_link_libraries(dcm2niix ${OPENJPEG_LIBRARIES}) -else () - add_definitions(-DmyDisableOpenJPEG) -endif() - -if(BATCH_VERSION) - set(DCM2NIIBATCH_SRCS - main_console_batch.cpp - nii_dicom.cpp - jpg_0XC3.cpp - ujpeg.cpp - nifti1_io_core.cpp - nii_foreign.cpp - nii_ortho.cpp - nii_dicom_batch.cpp) - - if(USE_JNIFTI) - set(DCM2NIIBATCH_SRCS ${DCM2NIIBATCH_SRCS} cJSON.cpp base64.cpp) - endif() - - if(USE_JPEGLS) - add_executable(dcm2niibatch ${DCM2NIIBATCH_SRCS} ${CHARLS_SRCS}) - else() - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") - add_executable(dcm2niibatch ${DCM2NIIBATCH_SRCS}) - endif() - - set(YAML-CPP_DIR ${YAML-CPP_DIR} CACHE PATH "Path to yaml-cpp configuration file" FORCE) - - find_package(YAML-CPP REQUIRED) - target_include_directories(dcm2niibatch PRIVATE ${YAML_CPP_INCLUDE_DIR}) - target_link_libraries(dcm2niibatch ${YAML_CPP_LIBRARIES}) - - if(ZLIB_FOUND) - target_include_directories(dcm2niibatch PRIVATE ${ZLIB_INCLUDE_DIRS}) - target_link_libraries(dcm2niibatch ${ZLIB_LIBRARIES}) - endif() - - if(TURBOJPEG_FOUND) - target_include_directories(dcm2niibatch PRIVATE ${TURBOJPEG_INCLUDEDIR}) - target_link_libraries(dcm2niibatch ${TURBOJPEG_LIBRARIES}) - endif() - - if(JASPER_FOUND) - target_include_directories(dcm2niibatch PRIVATE ${JASPER_INCLUDE_DIR}) - target_link_libraries(dcm2niibatch ${JASPER_LIBRARIES}) - endif() - - if(OPENJPEG_FOUND) - target_include_directories(dcm2niibatch PRIVATE ${OPENJPEG_INCLUDE_DIRS}) - target_link_libraries(dcm2niibatch ${OPENJPEG_LIBRARIES}) - endif() - - list(APPEND PROGRAMS dcm2niibatch) -endif() - -if(BUILD_DCM2NIIXFSLIB) - target_compile_definitions(${DCM2NIIXFSLIB} PRIVATE -DUSING_DCM2NIIXFSWRAPPER -DUSING_MGH_NIFTI_IO) -endif() - -if(APPLE) - message("-- Adding Apple plist") - set_target_properties(dcm2niix PROPERTIES LINK_FLAGS "-Wl,-sectcreate,__TEXT,__info_plist,${CMAKE_SOURCE_DIR}/Info.plist") - #Apple notarization requires a Info.plist - # 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 -endif() - -install(TARGETS ${PROGRAMS} DESTINATION bin) - -if(BUILD_DCM2NIIXFSLIB) - install(TARGETS ${DCM2NIIXFSLIB} DESTINATION lib) -endif() +cmake_minimum_required(VERSION 2.8.11) + +project(console) + +# Option Choose whether to use static runtime +include(ucm.cmake) +option(USE_STATIC_RUNTIME "Use static runtime" ON) +if(USE_STATIC_RUNTIME) + ucm_set_runtime(STATIC) +else() + ucm_set_runtime(DYNAMIC) +endif() + +# Basic CMake build settings +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Release" CACHE STRING + "Choose the type of build, options are: Debug Release RelWithDebInfo MinSizeRel." FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug;Release;RelWithDebInfo;MinSizeRel") +endif() + +if(${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") + # using Clang + add_definitions(-fno-caret-diagnostics) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-dead_strip -Wl,-stack_size -Wl,0x1000000") +elseif(${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU") + # using GCC + if(NOT (${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS 7.1.0)) + add_definitions(-Wno-format-overflow) # available since GCC 7.1.0 + endif() + if(NOT (${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS 4.5.0)) + add_definitions(-Wno-unused-result) # available since GCC 4.5.0 + endif() + if(NOT (${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS 4.8.0)) + add_definitions(-fno-diagnostics-show-caret) # available since GCC 4.8.0 + endif() +elseif(MSVC) + # using Visual Studio C++ + add_definitions(-D_CRT_SECURE_NO_DEPRECATE) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4018") # '<': signed/unsigned mismatch + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4068") # unknown pragma + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4101") # unreferenced local variable + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4244") # 'initializing': conversion from 'double' to 'int', possible loss of data + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4267") # 'initializing': conversion from 'size_t' to 'int', possible loss of data + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4305") # 'argument': truncation from 'double' to 'float' + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4308") # negative integral constant converted to unsigned type + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4334") # '<<': result of 32-bit shift implicitly converted to 64 bits + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4800") # 'uint32_t' : forcing value to bool 'true' or 'false' + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4819") # The file contains a character that cannot be represented in the current code page + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4996") # 'access': The POSIX name for this item is deprecated + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /STACK:16388608") # set "Stack Reserve Size" to 16MB (default value is 1MB) +endif() + +# Compiler dependent flags +include (CheckCXXCompilerFlag) +if(UNIX) + check_cxx_compiler_flag(-march=armv8-a+crc ARM_CRC) + if(ARM_CRC) + # wrong answer for Apple Silicon: check_cxx_compiler_flag(-msse2 HAS_SSE2) + else() + check_cxx_compiler_flag(-msse2 HAS_SSE2) + if(HAS_SSE2) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -msse2 -mfpmath=sse") + endif() + endif() +endif() + +set(PROGRAMS dcm2niix) + +option(USE_TURBOJPEG "Use TurboJPEG to decode classic JPEG" OFF) +option(USE_JASPER "Build with JPEG2000 support using Jasper" OFF) +option(USE_OPENJPEG "Build with JPEG2000 support using OpenJPEG" OFF) +option(USE_JPEGLS "Build with JPEG-LS support using CharLS" OFF) + +option(BATCH_VERSION "Build dcm2niibatch for multiple conversions" OFF) + +option(BUILD_DCM2NIIXFSLIB "Build libdcm2niixfs.a" OFF) + +if(USE_OPENJPEG OR USE_TURBOJPEG OR USE_JASPER) + message("-- Set BUILD_DCM2NIIXFSLIB to OFF since USE_TURBOJPEG/USE_JASPER/USE_OPENJPEG is ON.") + set(BUILD_DCM2NIIXFSLIB OFF CACHE BOOL "Build libdcm2niixfs.a" FORCE) +endif() + +set(DCM2NIIX_SRCS + main_console.cpp + nii_dicom.cpp + jpg_0XC3.cpp + ujpeg.cpp + nifti1_io_core.cpp + nii_foreign.cpp + nii_ortho.cpp + nii_dicom_batch.cpp) + + +option(USE_JNIfTI "Build with JNIfTI support" ON) +if(USE_JNIFTI) + add_definitions(-DmyEnableJNIfTI) + set(DCM2NIIX_SRCS ${DCM2NIIX_SRCS} cJSON.cpp base64.cpp) +endif() + +if(BUILD_DCM2NIIXFSLIB) + set(DCM2NIIXFSLIB dcm2niixfs) + set(DCM2NIIXFSLIB_SRCS + dcm2niix_fswrapper.cpp + nii_dicom.cpp + jpg_0XC3.cpp + ujpeg.cpp + nifti1_io_core.cpp + nii_foreign.cpp + nii_ortho.cpp + nii_dicom_batch.cpp) +endif() + +if(USE_JPEGLS) + add_definitions(-DmyEnableJPEGLS) + if(MSVC) + add_definitions(-DCHARLS_STATIC) + else() + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14") + endif() + + set(CHARLS_SRCS + charls/jpegls.cpp + charls/jpegmarkersegment.cpp + charls/interface.cpp + charls/jpegstreamwriter.cpp + charls/jpegstreamreader.cpp) + add_executable(dcm2niix ${DCM2NIIX_SRCS} ${CHARLS_SRCS}) + + if(BUILD_DCM2NIIXFSLIB) + add_library(${DCM2NIIXFSLIB} STATIC ${DCM2NIIXFSLIB_SRCS} ${CHARLS_SRCS}) + endif() +else() + add_executable(dcm2niix ${DCM2NIIX_SRCS}) + + if(BUILD_DCM2NIIXFSLIB) + add_library(${DCM2NIIXFSLIB} STATIC ${DCM2NIIXFSLIB_SRCS}) + endif() +endif() + +set(ZLIB_IMPLEMENTATION "Miniz" CACHE STRING "Choose zlib implementation.") +set_property(CACHE ZLIB_IMPLEMENTATION PROPERTY STRINGS "Miniz;System;Custom") +if(NOT ${ZLIB_IMPLEMENTATION} STREQUAL "Miniz") + if(NOT ${ZLIB_IMPLEMENTATION} STREQUAL "System") + set(ZLIB_ROOT ${ZLIB_ROOT} CACHE PATH "Specify custom zlib root directory.") + if(NOT ZLIB_ROOT) + message(FATAL_ERROR "ZLIB_ROOT needs to be set to locate custom zlib!") + endif() + endif() + find_package(ZLIB REQUIRED) + add_definitions(-DmyDisableMiniZ) + target_include_directories(dcm2niix PRIVATE ${ZLIB_INCLUDE_DIRS}) + target_link_libraries(dcm2niix ${ZLIB_LIBRARIES}) +endif() + +if(USE_TURBOJPEG) + find_package(PkgConfig REQUIRED) + pkg_check_modules(TURBOJPEG REQUIRED libturbojpeg) + add_definitions(-DmyTurboJPEG) + target_include_directories(dcm2niix PRIVATE ${TURBOJPEG_INCLUDEDIR}) + target_link_libraries(dcm2niix ${TURBOJPEG_LIBRARIES}) +endif() + +if(USE_JASPER) + find_package(Jasper REQUIRED) + add_definitions(-DmyEnableJasper) + target_include_directories(dcm2niix PRIVATE ${JASPER_INCLUDE_DIR}) + target_link_libraries(dcm2niix ${JASPER_LIBRARIES}) +endif() + +if(USE_OPENJPEG) + set(OpenJPEG_DIR "${OpenJPEG_DIR}" CACHE PATH "Path to OpenJPEG configuration file" FORCE) + + find_package(OpenJPEG REQUIRED) + + if(WIN32) + if(BUILD_SHARED_LIBS) + add_definitions(-DOPJ_EXPORTS) + else() + add_definitions(-DOPJ_STATIC) + endif() + endif() + + target_include_directories(dcm2niix PRIVATE ${OPENJPEG_INCLUDE_DIRS}) + target_link_libraries(dcm2niix ${OPENJPEG_LIBRARIES}) +else () + add_definitions(-DmyDisableOpenJPEG) +endif() + +if(BATCH_VERSION) + set(DCM2NIIBATCH_SRCS + main_console_batch.cpp + nii_dicom.cpp + jpg_0XC3.cpp + ujpeg.cpp + nifti1_io_core.cpp + nii_foreign.cpp + nii_ortho.cpp + nii_dicom_batch.cpp) + + if(USE_JNIFTI) + set(DCM2NIIBATCH_SRCS ${DCM2NIIBATCH_SRCS} cJSON.cpp base64.cpp) + endif() + + if(USE_JPEGLS) + add_executable(dcm2niibatch ${DCM2NIIBATCH_SRCS} ${CHARLS_SRCS}) + else() + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") + add_executable(dcm2niibatch ${DCM2NIIBATCH_SRCS}) + endif() + + set(YAML-CPP_DIR ${YAML-CPP_DIR} CACHE PATH "Path to yaml-cpp configuration file" FORCE) + + find_package(YAML-CPP REQUIRED) + if(YAML-CPP_FOUND AND NOT YAML_CPP_LIBRARIES) + # workaround for yaml-cpp-devel-0.7.0-1.fc38 on Fedora Rawhide + set(YAML_CPP_LIBRARIES yaml-cpp) + endif() + target_include_directories(dcm2niibatch PRIVATE ${YAML_CPP_INCLUDE_DIR}) + target_link_libraries(dcm2niibatch ${YAML_CPP_LIBRARIES}) + + if(ZLIB_FOUND) + target_include_directories(dcm2niibatch PRIVATE ${ZLIB_INCLUDE_DIRS}) + target_link_libraries(dcm2niibatch ${ZLIB_LIBRARIES}) + endif() + + if(TURBOJPEG_FOUND) + target_include_directories(dcm2niibatch PRIVATE ${TURBOJPEG_INCLUDEDIR}) + target_link_libraries(dcm2niibatch ${TURBOJPEG_LIBRARIES}) + endif() + + if(JASPER_FOUND) + target_include_directories(dcm2niibatch PRIVATE ${JASPER_INCLUDE_DIR}) + target_link_libraries(dcm2niibatch ${JASPER_LIBRARIES}) + endif() + + if(OPENJPEG_FOUND) + target_include_directories(dcm2niibatch PRIVATE ${OPENJPEG_INCLUDE_DIRS}) + target_link_libraries(dcm2niibatch ${OPENJPEG_LIBRARIES}) + endif() + + list(APPEND PROGRAMS dcm2niibatch) +endif() + +if(BUILD_DCM2NIIXFSLIB) + target_compile_definitions(${DCM2NIIXFSLIB} PRIVATE -DUSING_DCM2NIIXFSWRAPPER -DUSING_MGH_NIFTI_IO) +endif() + +if(APPLE) + message("-- Adding Apple plist") + set_target_properties(dcm2niix PROPERTIES LINK_FLAGS "-Wl,-sectcreate,__TEXT,__info_plist,${CMAKE_SOURCE_DIR}/Info.plist") + #Apple notarization requires a Info.plist + # 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 +endif() + +install(TARGETS ${PROGRAMS} DESTINATION bin) + +if(BUILD_DCM2NIIXFSLIB) + install(TARGETS ${DCM2NIIXFSLIB} DESTINATION lib) +endif() diff --git a/console/base64.cpp b/console/base64.cpp index e7033279..a645b77c 100644 --- a/console/base64.cpp +++ b/console/base64.cpp @@ -111,7 +111,8 @@ unsigned char * base64_decode(const unsigned char *src, size_t len, memset(dtable, 0x80, 256); //os_ for (i = 0; i < sizeof(base64_table) - 1; i++) dtable[base64_table[i]] = (unsigned char) i; - dtable['='] = 0; + //next line rewritten to avoid warning -Wchar-subscripts + dtable[61] = 0; //dtable['='] = 0; count = 0; for (i = 0; i < len; i++) { diff --git a/console/cJSON.cpp b/console/cJSON.cpp index bc95cde6..d694214d 100644 --- a/console/cJSON.cpp +++ b/console/cJSON.cpp @@ -95,7 +95,7 @@ CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item) { CJSON_PUBLIC(const char*) cJSON_Version(void) { static char version[15]; - sprintf(version, "%i.%i.%i", CJSON_VERSION_MAJOR, CJSON_VERSION_MINOR, CJSON_VERSION_PATCH); + snprintf(version, sizeof(version), "%i.%i.%i", CJSON_VERSION_MAJOR, CJSON_VERSION_MINOR, CJSON_VERSION_PATCH); return version; } @@ -505,22 +505,22 @@ static cJSON_bool print_number(const cJSON * const item, printbuffer * const out /* This checks for NaN and Infinity */ if ((d * 0) != 0) { - length = sprintf((char*)number_buffer, "null"); + length = snprintf((char*)number_buffer, sizeof(number_buffer), "null"); } else { /* Try 15 decimal places of precision to avoid nonsignificant nonzero digits */ - length = sprintf((char*)number_buffer, "%1.15g", d); + length = snprintf((char*)number_buffer, sizeof(number_buffer), "%1.15g", d); /* Check whether the original double can be recovered */ if ((sscanf((char*)number_buffer, "%lg", &test) != 1) || !compare_double((double)test, d)) { /* If not, print with 17 decimal places of precision */ - length = sprintf((char*)number_buffer, "%1.17g", d); + length = snprintf((char*)number_buffer, sizeof(number_buffer), "%1.17g", d); } } - /* sprintf failed or buffer overrun occurred */ + /* snprintf failed or buffer overrun occurred */ if ((length < 0) || (length > (int)(sizeof(number_buffer) - 1))) { return false; @@ -949,7 +949,7 @@ static cJSON_bool print_string_ptr(const unsigned char * const input, printbuffe break; default: /* escape and print as unicode codepoint */ - sprintf((char*)output_pointer, "u%04x", *input_pointer); + snprintf((char*)output_pointer, output_buffer->length - (output_pointer - output_buffer->buffer), "u%04x", *input_pointer); output_pointer += 4; break; } diff --git a/console/cJSON.h b/console/cJSON.h index 2c535628..204b40e0 100644 --- a/console/cJSON.h +++ b/console/cJSON.h @@ -258,7 +258,7 @@ CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * cons /* Minify a strings, remove blank characters(such as ' ', '\t', '\r', '\n') from strings. * The input pointer json cannot point to a read-only address area, such as a string constant, - * but should point to a readable and writable adress area. */ + * but should point to a readable and writable address area. */ CJSON_PUBLIC(void) cJSON_Minify(char *json); /* Helper functions for creating and adding items to an object at the same time. diff --git a/console/charls/README.md b/console/charls/README.md index 82f45adc..4ff0549d 100644 --- a/console/charls/README.md +++ b/console/charls/README.md @@ -1,6 +1,3 @@ -[![Build status](https://ci.appveyor.com/api/projects/status/yq0naf3v2m8nfa8r/branch/master?svg=true)](https://ci.appveyor.com/project/vbaderks/charls/branch/master) -[![Build Status](https://travis-ci.org/team-charls/charls.svg?branch=master)](https://travis-ci.org/team-charls/charls) - # CharLS CharLS is a C++ implementation of the JPEG-LS standard for lossless and near-lossless image compression and decompression. @@ -75,7 +72,7 @@ The code is regularly compiled/tested on Windows and 64 bit Linux. Additionally, ## Users & Acknowledgements -CharLS is being used by [GDCM DICOM toolkit](http://sourceforge.net/projects/gdcm/), thanks for [Mathieu Malaterre](http://sourceforge.net/users/malat) for getting CharLS started on Linux. [Kato Kanryu](http://knivez.homelinux.org/) wrote an initial version of the color transfroms and the DIB output format code, for an [irfanview](http://www.irfanview.com) plugin using CharLS. Thanks to Uli Schlachter, CharLS now finally runs correctly on big-endian architectures like Sun SPARC. +CharLS is being used by [GDCM DICOM toolkit](http://sourceforge.net/projects/gdcm/), thanks for [Mathieu Malaterre](http://sourceforge.net/users/malat) for getting CharLS started on Linux. [Kato Kanryu](http://knivez.homelinux.org/) wrote an initial version of the color transforms and the DIB output format code, for an [irfanview](http://www.irfanview.com) plugin using CharLS. Thanks to Uli Schlachter, CharLS now finally runs correctly on big-endian architectures like Sun SPARC. ## Legal diff --git a/console/charls/scan.h b/console/charls/scan.h index 8c3383e0..bcc261dc 100644 --- a/console/charls/scan.h +++ b/console/charls/scan.h @@ -106,7 +106,7 @@ class JlsCodec : public Strategy using PIXEL = typename Traits::PIXEL; using SAMPLE = typename Traits::SAMPLE; - WARNING_SUPPRESS(26495) // false warning that _contextRunmode is unintialized + WARNING_SUPPRESS(26495) // false warning that _contextRunmode is uninitialized JlsCodec(const Traits& inTraits, const JlsParameters& params) : Strategy(params), traits(inTraits), diff --git a/console/charls/util.h b/console/charls/util.h index 17914d28..573a07ba 100644 --- a/console/charls/util.h +++ b/console/charls/util.h @@ -27,7 +27,7 @@ std::unique_ptr make_unique(Args&&... args) #endif // Only use __forceinline for the Microsoft C++ compiler in release mode (verified scenario) -// Use the build-in optimizer for all other C++ compilers. +// Use the built-in optimizer for all other C++ compilers. // Note: usage of FORCE_INLINE may be reduced in the future as the latest generation of C++ compilers // can handle optimization by themselves. #ifndef FORCE_INLINE @@ -136,7 +136,7 @@ struct Quad : Triplet v4(0) {} - WARNING_SUPPRESS(26495) // false warning that v4 is unintialized + WARNING_SUPPRESS(26495) // false warning that v4 is uninitialized Quad(Triplet triplet, int32_t alpha) noexcept : Triplet(triplet), diff --git a/console/dcm2niix_fswrapper.cpp b/console/dcm2niix_fswrapper.cpp index 28db86d6..e8eedfc6 100644 --- a/console/dcm2niix_fswrapper.cpp +++ b/console/dcm2niix_fswrapper.cpp @@ -52,7 +52,7 @@ numSeries = 0 */ // set TDCMopts defaults, overwrite settings to output in mgz orientation -void dcm2niix_fswrapper::setOpts(const char* dcmindir, const char* niioutdir) +void dcm2niix_fswrapper::setOpts(const char* dcmindir, const char* niioutdir, bool createBIDS) { memset(&tdcmOpts, 0, sizeof(tdcmOpts)); setDefaultOpts(&tdcmOpts, NULL); @@ -62,13 +62,15 @@ void dcm2niix_fswrapper::setOpts(const char* dcmindir, const char* niioutdir) if (niioutdir != NULL) strcpy(tdcmOpts.outdir, niioutdir); + strcpy(tdcmOpts.filename, "%4s.%p"); + // set the options for freesurfer mgz orientation tdcmOpts.isRotate3DAcq = false; tdcmOpts.isFlipY = false; tdcmOpts.isIgnoreSeriesInstanceUID = true; - tdcmOpts.isCreateBIDS = false; + tdcmOpts.isCreateBIDS = createBIDS; tdcmOpts.isGz = false; - //tdcmOpts.isForceStackSameSeries = 1; // merge 2D slice '-m y' + tdcmOpts.isForceStackSameSeries = 1; // merge 2D slice '-m y' tdcmOpts.isForceStackDCE = false; //tdcmOpts.isForceOnsetTimes = false; } @@ -119,3 +121,11 @@ const unsigned char* dcm2niix_fswrapper::getMRIimg(void) return mrifsStruct->imgM; } +void dcm2niix_fswrapper::dicomDump(const char* dicomdir) +{ + strcpy(tdcmOpts.indir, dicomdir); + tdcmOpts.isDumpNotConvert = true; + nii_loadDirCore(tdcmOpts.indir, &tdcmOpts); + + return; +} diff --git a/console/dcm2niix_fswrapper.h b/console/dcm2niix_fswrapper.h index f9e45702..8cb0189f 100644 --- a/console/dcm2niix_fswrapper.h +++ b/console/dcm2niix_fswrapper.h @@ -17,7 +17,7 @@ class dcm2niix_fswrapper { public: // set TDCMopts defaults, overwrite settings to output in mgz orientation. - static void setOpts(const char* dcmindir, const char* niioutdir); + static void setOpts(const char* dcmindir, const char* niioutdir=NULL, bool createBIDS=false); // interface to isDICOMfile() in nii_dicom.cpp static bool isDICOM(const char* file); @@ -35,6 +35,8 @@ class dcm2niix_fswrapper // return image data saved in MRIFSSTRUCT static const unsigned char* getMRIimg(void); + static void dicomDump(const char* dicomdir); + private: static struct TDCMopts tdcmOpts; }; diff --git a/console/main_console.cpp b/console/main_console.cpp index 143995c6..539fd276 100644 --- a/console/main_console.cpp +++ b/console/main_console.cpp @@ -78,7 +78,7 @@ void showHelp(const char *argv[], struct TDCMopts opts) { printf(" -a : adjacent DICOMs (images from same series always in same folder) for faster conversion (n/y, default n)\n"); printf(" -b : BIDS sidecar (y/n/o [o=only: no NIfTI], default %c)\n", bool2Char(opts.isCreateBIDS)); 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(" -c : comment stored in NIfTI aux_file (up to 24 characters e.g. '-c VIP', empty to anonymize e.g. 0020,4000 e.g. '-c \"\"')\n"); printf(" -d : directory search depth. Convert DICOMs in sub-folders of in_folder? (0..9, default %d)\n", opts.dirSearchDepth); #ifdef myEnableJNIfTI printf(" -e : export as NRRD (y) or MGH (o) or JSON/JNIfTI (j) or BJNIfTI (b) instead of NIfTI (y/n/o/j/b, default n)\n"); @@ -104,6 +104,7 @@ void showHelp(const char *argv[], struct TDCMopts opts) { printf(" -n : only convert this series CRC number - can be used up to %i times (default convert all)\n", MAX_NUM_SERIES); printf(" -o : output directory (omit to save to input folder)\n"); printf(" -p : Philips precise float (not display) scaling (y/n, default y)\n"); + printf(" -q : only search directory for DICOMs (y/l/n, default y) [y=show number of DICOMs found, l=additionally list DICOMs found, n=no]\n"); printf(" -r : rename instead of convert DICOMs (y/n, default n)\n"); printf(" -s : single file mode, do not convert other images in folder (y/n, default n)\n"); //text notes replaced with BIDS: this function is deprecated @@ -178,7 +179,7 @@ void showHelp(const char *argv[], struct TDCMopts opts) { } //showHelp() int invalidParam(int i, const char *argv[]) { - if (strchr("yYnNoOhHiIjJBb01234",argv[i][0])) + if (strchr("yYnNoOhHiIjlLJBb01234",argv[i][0])) return 0; //if (argv[i][0] != '-') return 0; @@ -351,6 +352,8 @@ int main(int argc, const char *argv[]) { } else if ((argv[i][1] == 'c') && ((i + 1) < argc)) { i++; snprintf(opts.imageComments, 24, "%s", argv[i]); + if (strlen(opts.imageComments) == 0) //empty string is flag to anonymize DICOM image comments + snprintf(opts.imageComments, 24, "%s", "\t"); } else if ((argv[i][1] == 'd') && ((i + 1) < argc)) { i++; if ((argv[i][0] >= '0') && (argv[i][0] <= '9')) @@ -410,6 +413,25 @@ int main(int argc, const char *argv[]) { opts.isTestx0021x105E = true; printf("undocumented '-j y' compares GE slice timing from 0021,105E\n"); } + } else if ((!strcmp(argv[i], "--diffCyclingModeGE")) && ((i + 1) < argc)) { + // see issue 635 + i++; + if (argv[i][0] == '0') { + opts.diffCyclingModeGE = 0; + printf("undocumented '--diffCyclingModeGE 0' cycling OFF\n"); + } + else if (argv[i][0] == '1') { + opts.diffCyclingModeGE = 1; + printf("undocumented '--diffCyclingModeGE 1' cycling All-TR\n"); + } + else if (argv[i][0] == '2') { + opts.diffCyclingModeGE = 2; + printf("undocumented '--diffCyclingModeGE 2' cycling 2-TR\n"); + } + else if (argv[i][0] == '3') { + opts.diffCyclingModeGE = 3; + printf("undocumented '--diffCyclingModeGE 3' cycling 3-TR\n"); + } } else if ((argv[i][1] == 'l') && ((i + 1) < argc)) { i++; if (invalidParam(i, argv)) @@ -452,6 +474,14 @@ int main(int argc, const char *argv[]) { return 0; if ((argv[i][0] == 'y') || (argv[i][0] == 'Y')) opts.isRenameNotConvert = true; + } else if ((argv[i][1] == 'q') && ((i + 1) < argc)) { + i++; + if (invalidParam(i, argv)) + return 0; + if ((argv[i][0] == 'y') || (argv[i][0] == 'Y')) + opts.onlySearchDirForDICOM = 1; + else if ((argv[i][0] == 'l') || (argv[i][0] == 'L')) + opts.onlySearchDirForDICOM = 2; } else if ((argv[i][1] == 's') && ((i + 1) < argc)) { i++; if (invalidParam(i, argv)) @@ -605,11 +635,14 @@ int main(int argc, const char *argv[]) { if (ret != EXIT_SUCCESS) return ret; } + + if (opts.onlySearchDirForDICOM == 0) { #if !defined(_WIN64) && !defined(_WIN32) - printf("Conversion required %f seconds (%f for core code).\n", get_wall_time() - startWall, ((float)(clock() - start)) / CLOCKS_PER_SEC); + printf("Conversion required %f seconds (%f for core code).\n", get_wall_time() - startWall, ((float)(clock() - start)) / CLOCKS_PER_SEC); #else - printf("Conversion required %f seconds.\n", ((float)(clock() - start)) / CLOCKS_PER_SEC); + printf("Conversion required %f seconds.\n", ((float)(clock() - start)) / CLOCKS_PER_SEC); #endif + } //if (isSaveIni) //we now save defaults earlier, in case of early termination. // saveIniFile(opts); return EXIT_SUCCESS; diff --git a/console/makefile b/console/makefile index 3b14bfa9..1f35e23c 100644 --- a/console/makefile +++ b/console/makefile @@ -4,6 +4,12 @@ CFLAGS=-s -O3 # Debugging #CFLAGS=-g +# issue659: increase stack to 16mb +#For Linux linker stacksize ignored: we must use setrlimit +# LFLAGS=-Wl,-z -Wl,stack-size=16777216 +LFLAGS= + + #Leak tests: # https://clang.llvm.org/docs/AddressSanitizer.html # clang++ -O1 -g -fsanitize=address -fno-omit-frame-pointer -I. main_console.cpp nii_foreign.cpp nii_dicom.cpp jpg_0XC3.cpp ujpeg.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp base64.c cJSON.c -o dcm2niix -DmyDisableOpenJPEG @@ -34,7 +40,8 @@ ifneq ($(OS),Windows_NT) # 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 + LFLAGS=-Wl,-stack_size -Wl,0x1000000 endif endif all: - g++ $(CFLAGS) -I. $(JSFLAGS) $(JFLAGS) main_console.cpp nii_foreign.cpp nii_dicom.cpp jpg_0XC3.cpp ujpeg.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp -o dcm2niix -DmyDisableOpenJPEG + g++ $(CFLAGS) -I. $(JSFLAGS) $(JFLAGS) $(LFLAGS) main_console.cpp nii_foreign.cpp nii_dicom.cpp jpg_0XC3.cpp ujpeg.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp -o dcm2niix -DmyDisableOpenJPEG diff --git a/console/miniz.c b/console/miniz.c index 47d10109..133a3d32 100644 --- a/console/miniz.c +++ b/console/miniz.c @@ -705,7 +705,7 @@ enum // pSrc_buf, src_buf_len: Pointer and size of the Deflate or zlib source data to decompress. // On return: // Function returns a pointer to the decompressed data, or NULL on failure. -// *pOut_len will be set to the decompressed data's size, which could be larger than src_buf_len on uncompressible data. +// *pOut_len will be set to the decompressed data's size, which could be larger than src_buf_len on incompressible data. // The caller must call mz_free() on the returned block when it's no longer needed. void *tinfl_decompress_mem_to_heap(const void *pSrc_buf, size_t src_buf_len, size_t *pOut_len, int flags); @@ -817,7 +817,7 @@ enum // flags: The max match finder probes (default is 128) logically OR'd against the above flags. Higher probes are slower but improve compression. // On return: // Function returns a pointer to the compressed data, or NULL on failure. -// *pOut_len will be set to the compressed data's size, which could be larger than src_buf_len on uncompressible data. +// *pOut_len will be set to the compressed data's size, which could be larger than src_buf_len on incompressible data. // The caller must free() the returned block when it's no longer needed. void *tdefl_compress_mem_to_heap(const void *pSrc_buf, size_t src_buf_len, size_t *pOut_len, int flags); diff --git a/console/nii_dicom.cpp b/console/nii_dicom.cpp index 9c03232d..a14ea39c 100644 --- a/console/nii_dicom.cpp +++ b/console/nii_dicom.cpp @@ -1,5 +1,6 @@ //#define MY_DEBUG #if defined(_WIN64) || defined(_WIN32) +#define NOMINMAX #include //write to registry #endif #ifdef _MSC_VER @@ -36,6 +37,7 @@ #include #include // discriminate files from folders #include +#include #ifdef USING_R #undef isnan @@ -393,6 +395,39 @@ mat44 noNaN(mat44 Q44, bool isVerbose, bool *isBogus) //simplify any headers tha return ret; } +#define kYYYYMMDDlen 8 //how many characters to encode year,month,day in "YYYYDDMM" format + +/*double dicomTimeToSecX(double dicomTime) { + //convert HHMMSS to seconds, 135300.024 -> 135259.731 are 0.293 sec apart + char acqTimeBuf[64]; + snprintf(acqTimeBuf, sizeof acqTimeBuf, "%+013.5f", (double)dicomTime); + int ahour, amin; + double asec; + int count = 0; + sscanf(acqTimeBuf, "%3d%2d%lf%n", &ahour, &amin, &asec, &count); + if (!count) + return -1; + return (ahour * 3600) + (amin * 60) + asec; +} + +double DateTimeToTime(char *dstr) { + if (strlen(dstr) > (kYYYYMMDDlen + 5)) { + // 20161117131643.80000 -> date 20161117 time 131643.80000 + //printMessage("acquisitionDateTime %s\n",acquisitionDateTimeTxt); + //char acquisitionDateTxt[kDICOMStr]; + //memcpy(acquisitionDateTxt, dstr, kYYYYMMDDlen); + //acquisitionDateTxt[kYYYYMMDDlen] = '\0'; // IMPORTANT! + //d.acquisitionDate = atof(acquisitionDateTxt); + char acquisitionTimeTxt[kDICOMStr]; + int timeLen = (int)strlen(dstr) - kYYYYMMDDlen; + strncpy(acquisitionTimeTxt, &dstr[kYYYYMMDDlen], timeLen); + acquisitionTimeTxt[timeLen] = '\0'; // IMPORTANT! + printf(">>>%s\n", acquisitionTimeTxt); + return atof(acquisitionTimeTxt); + } + return 0.0; +}*/ + #define kSessionOK 0 #define kSessionBadMatrix 1 @@ -627,9 +662,9 @@ int headerDcm2NiiSForm(struct TDICOMdata d, struct TDICOMdata d2, struct nifti_1 d.orient[1] = 1.0f; d.orient[2] = 0.0f; d.orient[3] = 0.0f; - d.orient[1] = 0.0f; - d.orient[2] = 1.0f; - d.orient[3] = 0.0f; + d.orient[4] = 0.0f; + d.orient[5] = 1.0f; + d.orient[6] = 0.0f; if ((d.isDerived) || ((d.bitsAllocated == 8) && (d.samplesPerPixel == 3) && (d.manufacturer == kMANUFACTURER_SIEMENS))) { printMessage("Unable to determine spatial orientation: 0020,0037 missing (probably not a problem: derived image)\n"); } else { @@ -648,12 +683,12 @@ int headerDcm2Nii2(struct TDICOMdata d, struct TDICOMdata d2, struct nifti_1_hea if (h->slice_code == NIFTI_SLICE_UNKNOWN) h->slice_code = d2.CSA.sliceOrder; //sometimes the first slice order is screwed up https://github.com/eauerbach/CMRR-MB/issues/29 if (d.modality == kMODALITY_MR) - sprintf(txt, "TE=%.2g;Time=%.3f", d.TE, d.acquisitionTime); + snprintf(txt, 1024, "TE=%.2g;Time=%.3f", d.TE, d.acquisitionTime); else - sprintf(txt, "Time=%.3f", d.acquisitionTime); + snprintf(txt, 1024, "Time=%.3f", d.acquisitionTime); if (d.CSA.phaseEncodingDirectionPositive >= 0) { char dtxt[1024] = {""}; - sprintf(dtxt, ";phase=%d", d.CSA.phaseEncodingDirectionPositive); + snprintf(dtxt, 1024, ";phase=%d", d.CSA.phaseEncodingDirectionPositive); strcat(txt, dtxt); } //from dicm2nii 20151117 InPlanePhaseEncodingDirection @@ -663,15 +698,17 @@ int headerDcm2Nii2(struct TDICOMdata d, struct TDICOMdata d2, struct nifti_1_hea h->dim_info = (3 << 4) + (2 << 2) + 1; if (d.CSA.multiBandFactor > 1) { char dtxt[1024] = {""}; - sprintf(dtxt, ";mb=%d", d.CSA.multiBandFactor); + snprintf(dtxt, 1024, ";mb=%d", d.CSA.multiBandFactor); strcat(txt, dtxt); } // GCC 8 warns about truncation using snprintf // snprintf(h->descrip,80, "%s",txt); memcpy(h->descrip, txt, 79); h->descrip[79] = '\0'; - if (strlen(d.imageComments) > 0) + if ((strlen(d.imageComments) > 0) && (h->aux_file[0] == 0)) //issue691 snprintf(h->aux_file, 24, "%.23s", d.imageComments); + if ((h->aux_file[0] == '\t') && (h->aux_file[1] == 0)) + h->aux_file[0] = 0; //issue691 return headerDcm2NiiSForm(d, d2, h, isVerbose); } //headerDcm2Nii2() @@ -719,6 +756,7 @@ struct TDICOMdata clear_dicom_data() { strcpy(d.studyDate, ""); strcpy(d.studyTime, ""); strcpy(d.protocolName, ""); + strcpy(d.patientOrient, ""); strcpy(d.seriesDescription, ""); strcpy(d.sequenceName, ""); strcpy(d.scanningSequence, ""); @@ -799,11 +837,16 @@ struct TDICOMdata clear_dicom_data() { d.protocolBlockStartGE = 0; d.protocolBlockLengthGE = 0; d.phaseEncodingSteps = 0; + d.frequencyEncodingSteps = 0; + d.phaseEncodingStepsOutOfPlane = 0; d.coilCrc = 0; d.seriesUidCrc = 0; d.instanceUidCrc = 0; d.accelFactPE = 0.0; d.accelFactOOP = 0.0; + d.compressedSensingFactor = 0.0; + d.isDeepLearning = false; + strcpy(d.deepLearningText, ""); //d.patientPositionNumPhilips = 0; d.imageBytes = 0; d.intenScale = 1; @@ -868,6 +911,8 @@ struct TDICOMdata clear_dicom_data() { d.maxEchoNumGE = -1; d.epiVersionGE = -1; d.internalepiVersionGE = -1; + d.diffCyclingModeGE = kGE_DIFF_CYCLING_UNKNOWN; + d.tensorFileGE = 0; d.durationLabelPulseGE = -1; d.aslFlags = kASL_FLAG_NONE; d.partialFourierDirection = kPARTIAL_FOURIER_DIRECTION_UNKNOWN; @@ -883,6 +928,8 @@ struct TDICOMdata clear_dicom_data() { d.isHasOverlay = false; d.isPrivateCreatorRemap = false; d.isRealIsPhaseMapHz = false; + d.isVariableFlipAngle = false; + d.isQuadruped = false; d.numberOfImagesInGridUIH = 0; d.phaseEncodingRC = '?'; d.patientSex = '?'; @@ -903,6 +950,7 @@ struct TDICOMdata clear_dicom_data() { d.CSA.multiBandFactor = 1; d.CSA.SeriesHeader_offset = 0; d.CSA.SeriesHeader_length = 0; + d.CSA.coilNumber = -1; return d; } //clear_dicom_data() @@ -934,7 +982,7 @@ void dcmStrDigitsOnlyKey(char key, char *lStr) { return; bool isKey = false; for (int i = 0; i < (int)len; i++) { - if (!isdigit(lStr[i])) { + if (!isdigitdot(lStr[i])) { isKey = (lStr[i] == key); lStr[i] = ' '; } else if (!isKey) @@ -1208,6 +1256,10 @@ int dcmStrManufacturer(const int lByteLength, unsigned char lBuffer[]) { //read ret = kMANUFACTURER_UIH; if ((toupper(cString[0]) == 'B') && (toupper(cString[1]) == 'R')) ret = kMANUFACTURER_BRUKER; + if ((toupper(cString[0]) == 'M') && (toupper(cString[1]) == 'R')) + ret = kMANUFACTURER_MRSOLUTIONS; + if ((toupper(cString[0]) == 'H') && (toupper(cString[1]) == 'Y')) + ret = kMANUFACTURER_HYPERFINE; //if (ret == kMANUFACTURER_UNKNOWN) //reduce verbosity: single warning for series : Unable to determine manufacturer (0008,0070) // printWarning("Unknown manufacturer %s\n", cString); //#ifdef _MSC_VER @@ -1244,6 +1296,30 @@ float csaMultiFloat(unsigned char buff[], int nItems, float Floats[], int *Items return Floats[1]; } //csaMultiFloat() +int csaICEdims(unsigned char buff[]) { + //determine coil number from CSA header + //Combined images start with a letter: X_1_1_1_1_1_1_1_1_1_1_1_106 + //Coil data starts with coil number: 29_1_1_1_1_1_7_1_1_1_1_1_3000012 + TCSAitem itemCSA; + int lPos = 0; + memcpy(&itemCSA, &buff[lPos], sizeof(itemCSA)); + int coilNumber = -1; + if (itemCSA.xx2_Len > 0) { + lPos += sizeof(itemCSA); + char *cString = (char *)malloc(sizeof(char) * (itemCSA.xx2_Len)); + memcpy(cString, &buff[lPos], itemCSA.xx2_Len); //TPX memcpy(&cString, &buff[lPos], sizeof(cString)); + lPos += ((itemCSA.xx2_Len + 3) / 4) * 4; + char c = cString[0]; + if( c >= '0' && c <= '9' ){ + dcmStrDigitsOnly(cString); + char *end; + coilNumber = (int)strtol(cString, &end, 10); + } + free(cString); + } + return coilNumber; +} //csaICEdims() + bool csaIsPhaseMap(unsigned char buff[], int nItems) { //returns true if the tag "ImageHistory" has an item named "CC:ComplexAdd" TCSAitem itemCSA; @@ -1373,11 +1449,17 @@ int readCSAImageHeader(unsigned char *buff, int lLength, struct TCSAdata *CSA, i // Storage order is always little-endian, so byte-swap required values if necessary if (!littleEndianPlatform()) nifti_swap_4bytes(1, &tagCSA.nitems); + if (tagCSA.nitems > 128) { + printError("%d n_tags CSA Image Header corrupted (0029,1010) see issue 633.\n", tagCSA.nitems); + return EXIT_FAILURE; + } if (isVerbose > 1) //extreme verbosity: show every CSA tag printMessage(" %d CSA of %s %d\n", lPos, tagCSA.name, tagCSA.nitems); if (tagCSA.nitems > 0) { if (strcmp(tagCSA.name, "ImageHistory") == 0) CSA->isPhaseMap = csaIsPhaseMap(&buff[lPos], tagCSA.nitems); + else if (strcmp(tagCSA.name, "ICE_Dims") == 0) + CSA->coilNumber = csaICEdims(&buff[lPos]); else if (strcmp(tagCSA.name, "NumberOfImagesInMosaic") == 0) CSA->mosaicSlices = (int)round(csaMultiFloat(&buff[lPos], 1, lFloats, &itemsOK)); else if (strcmp(tagCSA.name, "B_value") == 0) { @@ -1484,6 +1566,15 @@ void dcmMultiFloat(int lByteLength, char lBuffer[], int lnFloats, float *lFloats free(cString); } //dcmMultiFloat() +double dcmStrDouble(const int lByteLength, const unsigned char lBuffer[]) { //read float stored as a string + char *cString = (char *)malloc(sizeof(char) * (lByteLength + 1)); + memcpy(cString, (char *)&lBuffer[0], lByteLength); + cString[lByteLength] = 0; //null terminate + double ret = (double)atof(cString); + free(cString); + return ret; +} //dcmStrDouble() + float dcmStrFloat(const int lByteLength, const unsigned char lBuffer[]) { //read float stored as a string char *cString = (char *)malloc(sizeof(char) * (lByteLength + 1)); memcpy(cString, (char *)&lBuffer[0], lByteLength); @@ -1802,6 +1893,7 @@ struct TDICOMdata nii_readParRec(char *parname, int isVerbose, struct TDTI4D *dt int numSlice2D = 0; int prevDyn = -1; bool dynNotAscending = false; + int maxSlice2D = 0; int parVers = 0; int maxSeq = -1; //maximum value of Seq column int seq1 = -1; //value of Seq volume for first slice @@ -2085,10 +2177,6 @@ struct TDICOMdata nii_readParRec(char *parname, int isVerbose, struct TDTI4D *dt if ((d.intenScale != cols[kRS]) || (d.intenIntercept != cols[kRI])) isIntenScaleVaries = true; } - if (cols[kImageType] == 0) - d.isHasMagnitude = true; - if (cols[kImageType] != 0) - d.isHasPhase = true; if (isSameFloat(cols[kImageType], 18)) { //printWarning("Field map in Hz will be saved as the 'real' image.\n"); //isTypeWarning = true; @@ -2193,13 +2281,25 @@ struct TDICOMdata nii_readParRec(char *parname, int isVerbose, struct TDTI4D *dt //if (slice == 1) printMessage("%d\t%d\t%d\t%d\t%d\n", isADC,(int)cols[kbvalNumber], (int)cols[kGradientNumber], bval, vol); if (vol > maxVol) maxVol = vol; - bool isReal = (cols[kImageType] == 1); - bool isImaginary = (cols[kImageType] == 2); - bool isPhase = (cols[kImageType] == 3); - if (cols[kImageType] == 18) { + int imageType = (int) cols[kImageType]; + bool isMagnitude = (imageType == 0); + bool isReal = (imageType == 1); + bool isImaginary = (imageType == 2); + bool isPhase = (imageType == 3); + bool isRealIsPhaseMapHz = (imageType == 18); + if (isMagnitude) + d.isHasMagnitude = true; + if (isImaginary) + d.isHasImaginary = true; + if (isPhase) + d.isHasPhase = true; + if (isRealIsPhaseMapHz) { isReal = true; + d.isHasReal = true; d.isRealIsPhaseMapHz = true; } + if (isReal) + d.isHasReal = true; if (cols[kImageType] == 4) { if (!isType4Warning) { printWarning("Unknown image type (4). Be aware the 'phase' image is of an unknown type.\n"); @@ -2262,7 +2362,7 @@ struct TDICOMdata nii_readParRec(char *parname, int isVerbose, struct TDTI4D *dt //offset images by type: mag+0,real+1, imag+2,phase+3 //if (cols[kImageType] != 0) //yikes - phase maps! // slice = slice + numExpected; - //printWarning("%d\t%d\n", slice -1, numSlice2D); + maxSlice2D = std::max(slice, maxSlice2D); if ((slice >= 0) && (slice < kMaxSlice2D) && (numSlice2D < kMaxSlice2D) && (numSlice2D >= 0)) { dti4D->sliceOrder[slice - 1] = numSlice2D; //printMessage("%d\t%d\t%d\n", numSlice2D, slice, (int)cols[kSlice],(int)vol); @@ -2318,11 +2418,22 @@ struct TDICOMdata nii_readParRec(char *parname, int isVerbose, struct TDTI4D *dt slice = slice + 1; } } + if (maxSlice2D >= kMaxSlice2D) {//issue659 + printError("Use dicm2nii or increase kMaxSlice2D (issue 659) %d\n", maxSlice2D); + d.isValid = false; + } + //number of image types: issue659 + int nType = 0; + if (d.isHasMagnitude) nType ++; + if (d.isHasImaginary) nType ++; + if (d.isHasPhase) nType ++; + if (d.isHasReal) nType ++; + nType = std::max(nType, 1); if (slice != numSlice2D) { printError("Catastrophic error: found %d but expected %d slices. %s\n", slice, numSlice2D, parname); - printMessage(" slices*grad*bval*cardiac*echo*dynamic*mix*labels = %d*%d*%d*%d*%d*%d*%d*%d\n", + printMessage(" slices*grad*bval*cardiac*echo*dynamic*mix*labels*types = %d*%d*%d*%d*%d*%d*%d*%d*%d\n", d.xyzDim[3], maxNumberOfGradientOrients, maxNumberOfDiffusionValues, - maxNumberOfCardiacPhases, maxNumberOfEchoes, maxNumberOfDynamics, maxNumberOfMixes, maxNumberOfLabels); + maxNumberOfCardiacPhases, maxNumberOfEchoes, maxNumberOfDynamics, maxNumberOfMixes, maxNumberOfLabels, nType); d.isValid = false; } for (int i = 0; i < numSlice2D; i++) { //issue363 @@ -3909,7 +4020,15 @@ void _update_tvd(struct TVolumeDiffusion *ptvd) { } } } - if (!isReady) { //bvecs NOT filled: see if symBMatrix filled + if ((!isReady) && (ptvd->_dtiV[0] < 100.0) && (!isnan(ptvd->_symBMatrix[0]))) { + //issue265: Bruker omits 0018,9089 for low b-values though it includes a bmatrix + ptvd->_dtiV[1] = 0.0; + ptvd->_dtiV[2] = 0.0; + ptvd->_dtiV[3] = 0.0; + isReady = true; + + } + /*if (!isReady) { //bvecs NOT filled: see if symBMatrix filled isReady = true; for (int i = 1; i < 6; ++i) if (isnan(ptvd->_symBMatrix[i])) @@ -3958,11 +4077,7 @@ void _update_tvd(struct TVolumeDiffusion *ptvd) { ptvd->_dtiV[1] = bVec.v[0]; ptvd->_dtiV[2] = bVec.v[1]; ptvd->_dtiV[3] = bVec.v[2]; - //printf("bmat=[%g %g %g %g %g %g %g %g %g]\n", ptvd->_symBMatrix[0],ptvd->_symBMatrix[1],ptvd->_symBMatrix[2], ptvd->_symBMatrix[1],ptvd->_symBMatrix[3],ptvd->_symBMatrix[4], ptvd->_symBMatrix[2],ptvd->_symBMatrix[4],ptvd->_symBMatrix[5]); - //printf("bmats=[%g %g %g %g %g %g];\n", ptvd->_symBMatrix[0],ptvd->_symBMatrix[1],ptvd->_symBMatrix[2],ptvd->_symBMatrix[3],ptvd->_symBMatrix[4],ptvd->_symBMatrix[5]); - //printf("bvec=[%g %g %g];\n", ptvd->_dtiV[1], ptvd->_dtiV[2], ptvd->_dtiV[3]); - //printf("bval=%g;\n\n", ptvd->_dtiV[0]); - } + }*/ if (!isReady) return; // If still here, update dd and *pdti4D. @@ -4095,6 +4210,7 @@ struct TDICOMdata readDICOMx(char *fname, struct TDCMprefs *prefs, struct TDTI4D dti4D->volumeOnsetTime[0] = -1; dti4D->decayFactor[0] = -1; dti4D->frameDuration[0] = -1; + dti4D->frameReferenceTime[0] = -1; //dti4D->fragmentOffset[0] = -1; dti4D->intenScale[0] = 0.0; struct TVolumeDiffusion volDiffusion = initTVolumeDiffusion(&d, dti4D); @@ -4226,7 +4342,7 @@ struct TDICOMdata readDICOMx(char *fname, struct TDCMprefs *prefs, struct TDTI4D #define kEchoNum 0x0018 + (0x0086 << 16) //IS #define kMagneticFieldStrength 0x0018 + (0x0087 << 16) //DS #define kZSpacing 0x0018 + (0x0088 << 16) //'DS' 'SpacingBetweenSlices' -#define kPhaseEncodingSteps 0x0018 + (0x0089 << 16) //'IS' +#define kPhaseEncodingSteps 0x0018 + (0x0089 << 16) //IS #define kEchoTrainLength 0x0018 + (0x0091 << 16) //IS #define kPercentSampling 0x0018 + (0x0093 << 16) //'DS' #define kPhaseFieldofView 0x0018 + (0x0094 << 16) //'DS' @@ -4247,8 +4363,9 @@ struct TDICOMdata readDICOMx(char *fname, struct TDCMprefs *prefs, struct TDTI4D #define kReceiveCoilName 0x0018 + (0x1250 << 16) // SH //#define kTransmitCoilName 0x0018 + (0x1251 << 16) // SH issue527 #define kAcquisitionMatrix 0x0018 + (0x1310 << 16) //US -#define kFlipAngle 0x0018 + (0x1314 << 16) #define kInPlanePhaseEncodingDirection 0x0018 + (0x1312 << 16) //CS +#define kFlipAngle 0x0018 + (0x1314 << 16) +#define kVariableFlipAngleFlag 0x0018 + (0x1315 << 16) //CS #define kSAR 0x0018 + (0x1316 << 16) //'DS' 'SAR' #define kPatientOrient 0x0018 + (0x5100 << 16) //0018,5100. patient orientation - 'HFS' #define kPulseSequenceName 0x0018 + uint32_t(0x9005 << 16) //'SH' 'YES'/'NO' @@ -4259,30 +4376,34 @@ struct TDICOMdata readDICOMx(char *fname, struct TDCMprefs *prefs, struct TDTI4D #define kRectilinearPhaseEncodeReordering 0x0018 + uint32_t(0x9034 << 16) //'CS' 'REVERSE_LINEAR'/'LINEAR' #define kPartialFourierDirection 0x0018 + uint32_t(0x9036 << 16) //'CS' #define kCardiacSynchronizationTechnique 0x0018 + uint32_t(0x9037 << 16) //'CS' +#define kMRAcquisitionFrequencyEncodingSteps 0x0018 + uint32_t(0x9058 << 16) //US #define kParallelReductionFactorInPlane 0x0018 + uint32_t(0x9069 << 16) //FD #define kAcquisitionDuration 0x0018 + uint32_t(0x9073 << 16) //FD -//#define kFrameAcquisitionDateTime 0x0018+uint32_t(0x9074<< 16 ) //DT "20181019212528.232500" +#define kFrameAcquisitionDateTime 0x0018+uint32_t(0x9074<< 16 ) //DT "20181019212528.232500" #define kDiffusionDirectionality 0x0018 + uint32_t(0x9075 << 16) // NONE, ISOTROPIC, or DIRECTIONAL #define kParallelAcquisitionTechnique 0x0018 + uint32_t(0x9078 << 16) //CS: SENSE, SMASH #define kInversionTimes 0x0018 + uint32_t(0x9079 << 16) //FD #define kPartialFourier 0x0018 + uint32_t(0x9081 << 16) //CS -const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); +const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD //#define kDiffusionBFactorSiemens 0x0019+(0x100C<< 16 ) // 0019;000C;SIEMENS MR HEADER;B_value #define kDiffusion_bValue 0x0018 + uint32_t(0x9087 << 16) // FD #define kDiffusionOrientation 0x0018 + uint32_t(0x9089 << 16) // FD, seen in enhanced DICOM from Philips 5.* and Siemens XA10. -#define kImagingFrequency2 0x0018 + uint32_t(0x9098 << 16) //FD +#define kImagingFrequencyFD 0x0018 + uint32_t(0x9098 << 16) //FD +#define kMREchoSequence 0x0018 + uint32_t(0x9114 << 16) //SQ #define kParallelReductionFactorOutOfPlane 0x0018 + uint32_t(0x9155 << 16) //FD +#define kSARFD 0x0018 + uint32_t(0x9181 << 16) //FD +#define kMRAcquisitionPhaseEncodingStepsInPlane 0x0018 + uint32_t(0x9231 << 16) //US +#define kMRAcquisitionPhaseEncodingStepsOutOfPlane 0x0018 + uint32_t(0x9232 << 16) //US +#define kGradientEchoTrainLength 0x0018 + uint32_t(0x9241 << 16) //US //#define kFrameAcquisitionDuration 0x0018+uint32_t(0x9220 << 16 ) //FD #define kArterialSpinLabelingContrast 0x0018 + uint32_t(0x9250 << 16) //CS #define kASLPulseTrainDuration 0x0018 + uint32_t(0x9258 << 16) //UL #define kDiffusionBValueXX 0x0018 + uint32_t(0x9602 << 16) //FD -#define kDiffusionBValueXY 0x0018 + uint32_t(0x9603 << 16) //FD -#define kDiffusionBValueXZ 0x0018 + uint32_t(0x9604 << 16) //FD -#define kDiffusionBValueYY 0x0018 + uint32_t(0x9605 << 16) //FD -#define kDiffusionBValueYZ 0x0018 + uint32_t(0x9606 << 16) //FD -#define kDiffusionBValueZZ 0x0018 + uint32_t(0x9607 << 16) //FD -#define kMREchoSequence 0x0018 + uint32_t(0x9114 << 16) //SQ -#define kMRAcquisitionPhaseEncodingStepsInPlane 0x0018 + uint32_t(0x9231 << 16) //US +//#define kDiffusionBValueXY 0x0018 + uint32_t(0x9603 << 16) //FD +//#define kDiffusionBValueXZ 0x0018 + uint32_t(0x9604 << 16) //FD +//#define kDiffusionBValueYY 0x0018 + uint32_t(0x9605 << 16) //FD +//#define kDiffusionBValueYZ 0x0018 + uint32_t(0x9606 << 16) //FD +//#define kDiffusionBValueZZ 0x0018 + uint32_t(0x9607 << 16) //FD #define kNumberOfImagesInMosaic 0x0019 + (0x100A << 16) //US NumberOfImagesInMosaic //https://nmrimaging.wordpress.com/2011/12/20/when-we-process/ // https://nciphub.org/groups/qindicom/wiki/DiffusionrelatedDICOMtags:experienceacrosssites?action=pdf @@ -4297,11 +4418,14 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); #define kInternalPulseSequenceNameGE 0x0019 + (0x109E << 16) //LO 'EPI' or 'EPI2' #define kRawDataRunNumberGE 0x0019 + (0x10A2 << 16)//SL #define kMaxEchoNumGE 0x0019 + (0x10A9 << 16) //DS -#define kUserData12GE 0x0019 + (0x10B3 << 16) //DS phase diffusion direction +#define kUserData11GE 0x0019 + (0x10B2 << 16) //DS Diffusion tensor filename +#define kUserData12GE 0x0019 + (0x10B3 << 16) //DS phase diffusion direction; diffusion gradient cycling mode +#define kUserData15GE 0x0019 + (0x10B6 << 16) //DS Diffusion Gradient Derating; cycling special OFF #define kDiffusionDirectionGEX 0x0019 + (0x10BB << 16) //DS phase diffusion direction #define kDiffusionDirectionGEY 0x0019 + (0x10BC << 16) //DS frequency diffusion direction #define kDiffusionDirectionGEZ 0x0019 + (0x10BD << 16) //DS slice diffusion direction -#define kNumberOfDiffusionDirectionGE 0x0019 + (0x10E0 << 16) ///DS NumberOfDiffusionDirection:UserData24 +#define kNumberOfDiffusionT2GE 0x0019 + (0x10DF << 16) ///DS NumberOfDiffusionT2:UserData23 (release 10+) +#define kNumberOfDiffusionDirectionGE 0x0019 + (0x10E0 << 16) ///DS NumberOfDiffusionDirection:UserData24 (release 10+) #define kVelocityEncodeScaleGE 0x0019 + (0x10E2 << 16) ///DS Velocity Encode Scale #define kStudyID 0x0020 + (0x0010 << 16) #define kSeriesNum 0x0020 + (0x0011 << 16) @@ -4325,7 +4449,7 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); #define kTemporalPositionIndex 0x0020 + uint32_t(0x9128 << 16) // UL #define kDimensionIndexPointer 0x0020 + uint32_t(0x9165 << 16) //Private Group 21 as Used by Siemens: -#define kScanningSequenceSiemens 0x0021 + (0x105A << 16) //CS +#define kScanningSequenceSiemens 0x0021 + (0x105A << 16) //CS n.b. for GE this is Diffusion direction of SL! #define kSequenceVariant21 0x0021 + (0x105B << 16) //CS Siemens ONLY: For GE this is TaggingFlipAngle #define kScanOptionsSiemens 0x0021 + (0x105C << 16) //CS Siemens ONLY #define kPATModeText 0x0021 + (0x1009 << 16) //LO, see kImaPATModeText @@ -4334,10 +4458,12 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); #define kICE_dims 0x0021 + (0x1106 << 16) //LO [X_4_1_1_1_1_160_1_1_1_1_1_277] #define kPhaseEncodingDirectionPositiveSiemens 0x0021 + (0x111C << 16) //IS #define kRealDwellTime 0x0021+(0x1142<< 16 )//IS +//#define kPATModeText2 0x0021 + (0x1156 << 16) //LO, always same as 0021,1009 #define kBandwidthPerPixelPhaseEncode21 0x0021 + (0x1153 << 16) //FD #define kCoilElements 0x0021 + (0x114F << 16) //LO #define kAcquisitionMatrixText21 0x0021 + (0x1158 << 16) //SH #define kImageTypeText 0x0021 + (0x1175 << 16) //CS +#define kDeepLearningText 0x0021 + (0x1176 << 16) //LO //Private Group 21 as used by GE: #define kLocationsInAcquisitionGE 0x0021 + (0x104F << 16) //SS 'LocationsInAcquisitionGE' #define kRTIA_timer 0x0021 + (0x105E << 16) //DS @@ -4373,6 +4499,7 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); #define kShimGradientX 0x0043 + (0x1002 << 16) //SS #define kShimGradientY 0x0043 + (0x1003 << 16) //SS #define kShimGradientZ 0x0043 + (0x1004 << 16) //SS +#define kVasCollapseFlagGE 0x0043 + (0x1030 << 16) //SS issue690 #define kPrescanReuseString 0x0043 + (0x1095 << 16) //LO #define kUserDefineDataGE 0x0043 + (0x102A << 16) //OB #define kEffectiveEchoSpacingGE 0x0043 + (0x102C << 16) //SS @@ -4384,6 +4511,8 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); #define kASLLabelingTechniqueGE 0x0043 + (0x10A4 << 16) //LO #define kDurationLabelPulseGE 0x0043 + (0x10A5 << 16) //IS #define kMultiBandGE 0x0043 + (0x10B6 << 16) //LO +#define kCompressedSensingParameters 0x0043 + (0x10B7 << 16) //LO +#define kDeepLearningParameters 0x0043 + (0x10CA << 16) //LO "0.75\High" #define kAcquisitionMatrixText 0x0051 + (0x100B << 16) //LO #define kImageOrientationText 0x0051 + (0x100E << 16) // #define kCoilSiemens 0x0051 + (0x100F << 16) @@ -4428,6 +4557,7 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); //#define kNumberOfLocationsPhilips 0x2001+(0x1015 << 16 ) //SS //#define kStackSliceNumber 0x2001+(0x1035 << 16 )//? Potential way to determine slice order for Philips? #define kNumberOfDynamicScans 0x2001 + (0x1081 << 16) //'2001' '1081' 'IS' 'NumberOfDynamicScans' +//#define kTRPhilips 0x2005 + (0x1030 << 16) //(2005,1030) FL 30\150 #define kMRfMRIStatusIndicationPhilips 0x2005 + (0x1063 << 16) #define kMRAcquisitionTypePhilips 0x2005 + (0x106F << 16) //SS #define kAngulationAP 0x2005 + (0x1071 << 16) //'2005' '1071' 'FL' 'MRStackAngulationAP' @@ -4437,10 +4567,11 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); #define kMRStackOffcentreFH 0x2005 + (0x1079 << 16) #define kMRStackOffcentreRL 0x2005 + (0x107A << 16) #define kPhilipsSlope 0x2005 + (0x100E << 16) -#define kMRImageDynamicScanBeginTime 0x2005 + (0x10a0 << 16) //FL +#define kMRImageDynamicScanBeginTime 0x2005 + (0x10A0 << 16) //FL #define kDiffusionDirectionRL 0x2005 + (0x10B0 << 16) #define kDiffusionDirectionAP 0x2005 + (0x10B1 << 16) #define kDiffusionDirectionFH 0x2005 + (0x10B2 << 16) +#define kDeepLearningPhilips 0x2005 + (0x1110 << 16) #define kPrivatePerFrameSq 0x2005 + (0x140F << 16) #define kMRImageDiffBValueNumber 0x2005 + (0x1412 << 16) //IS #define kMRImageGradientOrientationNumber 0x2005+(0x1413 << 16) //IS @@ -4463,20 +4594,26 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); // https://github.com/dcm4che/dcm4che/blob/master/dcm4che-dict/src/main/dicom3tools/libsrc/standard/elmdict/siemens.tpl // https://github.com/neurolabusc/dcm_qa_agfa // http://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_7.8.html -#define kMaxRemaps 16 //no vendor uses more than 5 private creator groups + #define kMaxRemaps 16 //no vendor uses more than 5 private creator groups //we need to keep track of multiple remappings, e.g. issue 437 2005,0014->2005,0012; 2005,0015->2005,0011 int nRemaps = 0; uint32_t privateCreatorMasks[kMaxRemaps]; //0 -> none uint32_t privateCreatorRemaps[kMaxRemaps]; //0 -> none #endif + double maxSAR = -INFINITY; double TE = 0.0; //most recent echo time recorded float temporalResolutionMS = 0.0; float MRImageDynamicScanBeginTime = 0.0; + bool isHasBMatrix = false; + bool isHasBVec = false; bool is2005140FSQ = false; bool is4000561SQ = false; //Original Attributes SQ bool is00089092SQ = false; //Referenced Image Evidence SQ bool overlayOK = true; + int userData11GE = 0; int userData12GE = 0; + float userData15GE = 0; + float accelFactPE = 0.0; int overlayRows = 0; int overlayCols = 0; bool isNeologica = false; @@ -4491,6 +4628,7 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); int philMRImageDiffBValueNumber = 0; int philMRImageDiffVolumeNumber = -1; int sqDepth = 0; + int seriesInstanceUIDsqDepth = 65535; //issue655 int acquisitionTimesGE_UIH = 0; int sqDepth00189114 = -1; bool hasDwiDirectionality = false; @@ -4505,6 +4643,7 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); size_t dimensionIndexPointerCounter = 0; int maxInStackPositionNumber = 0; int temporalPositionIndex = 0; + int minTemporalPositionIndex = 65535; int maxTemporalPositionIndex = 0; //int temporalPositionIdentifier = 0; int locationsInAcquisitionPhilips = 0; @@ -4549,6 +4688,7 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); char scanningSequenceSiemens[kDICOMStr] = ""; char imageType1st[kDICOMStr] = ""; bool isEncapsulatedData = false; + int diffusionDirectionTypeGE = 0; //issue690 int multiBandFactor = 0; int frequencyRows = 0; int numberOfImagesInMosaic = 0; @@ -4572,6 +4712,7 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); float vRLPhilips = 0.0; float vAPPhilips = 0.0; float vFHPhilips = 0.0; + //float TRPhilips = -1.0; double acquisitionTimePhilips = -1.0; bool isPhase = false; bool isReal = false; @@ -5033,6 +5174,7 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); // d.isValid = false; //return d; } + if (lLength > 0) //issue695: skip empty tags, "gdcmanon --dumb --empty 0018,0089 good.dcm bad.dcm" switch (groupElement) { case kMediaStorageSOPClassUID: { char mediaUID[kDICOMStr]; @@ -5252,7 +5394,7 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); char uid[kDICOMStrLarge]; dcmStr(lLength, &buffer[lPos], uid, true); char *timeStr = strrchr(uid, '.'); - //nb Manufactuer (0008,0070) comes AFTER (0008,0018) SOPInstanceUID. + //nb Manufacturer (0008,0070) comes AFTER (0008,0018) SOPInstanceUID. //format of (0008,0018) UI //[1.23.4.2019051416101221842 // .YYYYMMDDHHmmssxxxxx @@ -5304,6 +5446,8 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); dcmStr(lLength, &buffer[lPos], d.referringPhysicianName); break; case kReferencedImageEvidenceSQ: + if (lLength > 8) + break; //issue639: we will skip entire icon if there is an explicit length is00089092SQ = true; break; case kComplexImageComponent: @@ -5375,7 +5519,8 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); int slen = (int)strlen(aotTxt); if ((slen < 9) || (strstr(aotTxt, "QUADRUPED") == NULL)) break; - printError("Anatomical Orientation Type (0010,2210) is QUADRUPED: rotate coordinates accordingly\n"); + d.isQuadruped = true; + //printError("Anatomical Orientation Type (0010,2210) is QUADRUPED: rotate coordinates accordingly\n"); break; } case kDeidentificationMethod: { //issue 383 @@ -5502,9 +5647,9 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); if (lLength < 2) break; if (toupper(buffer[lPos]) == 'L') - d.phaseEncodingGE = kGE_PHASE_ENCODING_POLARITY_UNFLIPPED; + d.phaseEncodingGE = kGE_PHASE_ENCODING_POLARITY_FLIPPED; //issue674, (0018, 9034) LINEAR-->FLIPPED if (toupper(buffer[lPos]) == 'R') - d.phaseEncodingGE = kGE_PHASE_ENCODING_POLARITY_FLIPPED; + d.phaseEncodingGE = kGE_PHASE_ENCODING_POLARITY_UNFLIPPED; //issue674, R(0018, 9034) REVERSE_LINEAR-->UNFLIPPED break; } case kPartialFourierDirection: { //'CS' PHASE FREQUENCY SLICE_SELECT COMBINATION @@ -5525,22 +5670,25 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); isProspectiveSynced = true; break;*/ case kParallelReductionFactorInPlane: + accelFactPE = dcmFloatDouble(lLength, &buffer[lPos], d.isLittleEndian); if (d.manufacturer == kMANUFACTURER_SIEMENS) break; - d.accelFactPE = dcmFloatDouble(lLength, &buffer[lPos], d.isLittleEndian); + d.accelFactPE = accelFactPE; break; case kAcquisitionDuration: //n.b. used differently by different vendors https://github.com/rordenlab/dcm2niix/issues/225 d.acquisitionDuration = dcmFloatDouble(lLength, &buffer[lPos], d.isLittleEndian); break; //in theory, 0018,9074 could provide XA10 slice time information, but scrambled by XA10 de-identification: better to use 0021,1104 - //case kFrameAcquisitionDateTime: { - // //(0018,9074) DT [20190621095516.140000] YYYYMMDDHHMMSS - // //see https://github.com/rordenlab/dcm2niix/issues/303 - // char dateTime[kDICOMStr]; - // dcmStr(lLength, &buffer[lPos], dateTime); - // printf("%s\tkFrameAcquisitionDateTime\n", dateTime); - //} + /*case kFrameAcquisitionDateTime: { + //(0018,9074) DT [20190621095516.140000] YYYYMMDDHHMMSS + //see https://github.com/rordenlab/dcm2niix/issues/303 + char dateTime[kDICOMStr]; + dcmStr(lLength, &buffer[lPos], dateTime); + double dTime = DateTimeToTime(dateTime); + printf("%s\t FrameAcquisitionDateTime %0.4f \n", dateTime, dTime); + //d.triggerDelayTime = dTime; + }*/ case kDiffusionDirectionality: { // 0018, 9075 set_directionality0018_9075(&volDiffusion, (&buffer[lPos])); if ((d.manufacturer != kMANUFACTURER_PHILIPS) || (lLength < 10)) @@ -5574,7 +5722,16 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); sqDepth00189114 = sqDepth - 1; break; case kMRAcquisitionPhaseEncodingStepsInPlane: - d.phaseEncodingLines = dcmInt(lLength, &buffer[lPos], d.isLittleEndian); + d.phaseEncodingSteps = dcmInt(lLength, &buffer[lPos], d.isLittleEndian); + break; + case kMRAcquisitionFrequencyEncodingSteps: + d.frequencyEncodingSteps = dcmInt(lLength, &buffer[lPos], d.isLittleEndian); + break; + case kMRAcquisitionPhaseEncodingStepsOutOfPlane: + d.phaseEncodingStepsOutOfPlane = dcmInt(lLength, &buffer[lPos], d.isLittleEndian); + break; + case kGradientEchoTrainLength: + d.echoTrainLength = dcmInt(lLength, &buffer[lPos], d.isLittleEndian); break; case kNumberOfImagesInMosaic: if (d.manufacturer == kMANUFACTURER_SIEMENS) @@ -5628,6 +5785,13 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); d.CSA.dtiV[3] = v[2]; break; } + case kNumberOfDiffusionT2GE: { + if (d.manufacturer != kMANUFACTURER_GE) + break; + float f = dcmStrFloat(lLength, &buffer[lPos]); + d.numberOfDiffusionT2GE = round(f); + break; + } case kNumberOfDiffusionDirectionGE: { if (d.manufacturer != kMANUFACTURER_GE) break; @@ -5660,12 +5824,22 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); break; d.maxEchoNumGE = round(dcmStrFloat(lLength, &buffer[lPos])); break; + case kUserData11GE: { + if (d.manufacturer != kMANUFACTURER_GE) + break; + userData11GE = round(dcmStrFloat(lLength, &buffer[lPos])); + break; } case kUserData12GE: { if (d.manufacturer != kMANUFACTURER_GE) break; userData12GE = round(dcmStrFloat(lLength, &buffer[lPos])); //printf("%d<<<<\n", userData12GE); break; } + case kUserData15GE: { + if (d.manufacturer != kMANUFACTURER_GE) + break; + userData15GE = dcmStrFloat(lLength, &buffer[lPos]); + break; } case kDiffusionDirectionGEX: if (d.manufacturer == kMANUFACTURER_GE) set_diffusion_directionGE(&volDiffusion, lLength, (&buffer[lPos]), 0); @@ -5722,8 +5896,9 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); dcmStr(lLength, &buffer[lPos], d.studyInstanceUID); break; case kSeriesInstanceUID: // 0020,000E + if (sqDepth > seriesInstanceUIDsqDepth) break; //issue655 + seriesInstanceUIDsqDepth = sqDepth; dcmStr(lLength, &buffer[lPos], d.seriesInstanceUID); - //printMessage(">>%s\n", d.seriesInstanceUID); d.seriesUidCrc = mz_crc32X((unsigned char *)&d.seriesInstanceUID, strlen(d.seriesInstanceUID)); break; case kImagePositionPatient: { @@ -5755,8 +5930,8 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); } //if not first slice in file set_isAtFirstPatientPosition_tvd(&volDiffusion, isAtFirstPatientPosition); //if (isAtFirstPatientPosition) numFirstPatientPosition++; - if (isVerbose > 0) //verbose > 1 will report full DICOM tag - printMessage(" Patient Position 0020,0032 (#,@,X,Y,Z)\t%d\t%ld\t%g\t%g\t%g\n", patientPositionNum, lPos, patientPosition[1], patientPosition[2], patientPosition[3]); + if (isVerbose > 1) //verbose > 1 will report full DICOM tag + printMessage(" Patient Position 0020,0032 (#,@,X,Y,Z)\t%d\t%zu\t%g\t%g\t%g\n", patientPositionNum, lPos, patientPosition[1], patientPosition[2], patientPosition[3]); if ((isOrient) && (nSliceMM < kMaxSlice2D)) { vec3 pos = setVec3(patientPosition[1], patientPosition[2], patientPosition[3]); sliceMM[nSliceMM] = dotProduct(pos, sliceV); @@ -5779,6 +5954,7 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); break; case kSAR: d.SAR = dcmStrFloat(lLength, &buffer[lPos]); + maxSAR = fmax(maxSAR, d.SAR); break; case kStudyID: dcmStr(lLength, &buffer[lPos], d.studyID); @@ -5809,6 +5985,8 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); temporalPositionIndex = dcmInt(4, &buffer[lPos], d.isLittleEndian); if (temporalPositionIndex > maxTemporalPositionIndex) maxTemporalPositionIndex = temporalPositionIndex; + if (temporalPositionIndex < minTemporalPositionIndex) + minTemporalPositionIndex = temporalPositionIndex; break; case kDimensionIndexPointer: dimensionIndexPointer[dimensionIndexPointerCounter++] = dcmAttributeTag(&buffer[lPos], d.isLittleEndian); @@ -5854,6 +6032,8 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); d.isPlanarRGB = dcmInt(lLength, &buffer[lPos], d.isLittleEndian); break; case kDim3: + if (lLength < 1) //issue 695 + break; d.xyzDim[3] = dcmStrInt(lLength, &buffer[lPos]); numberOfFrames = d.xyzDim[3]; break; @@ -5885,13 +6065,13 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); char accelStr[kDICOMStr]; dcmStr(lLength, &buffer[lPos], accelStr); char *ptr; - dcmStrDigitsOnlyKey('p', accelStr); //e.g. if "p2s4" return "2", if "s4" return "" + dcmStrDigitsDotOnlyKey('p', accelStr); //e.g. if "p2s4" return "2", if "s4" return "" d.accelFactPE = (float)strtof(accelStr, &ptr); if (*ptr != '\0') d.accelFactPE = 0.0; //between slice accel dcmStr(lLength, &buffer[lPos], accelStr); - dcmStrDigitsOnlyKey('s', accelStr); //e.g. if "p2s4" return "4", if "p2" return "" + dcmStrDigitsDotOnlyKey('s', accelStr); //e.g. if "p2s4" return "4", if "p2" return "" multiBandFactor = (int)strtol(accelStr, &ptr, 10); if (*ptr != '\0') multiBandFactor = 0.0; @@ -5916,7 +6096,7 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); break; d.CSA.sliceTiming[acquisitionTimesGE_UIH] = dcmStrFloat(lLength, &buffer[lPos]); d.CSA.sliceTiming[acquisitionTimesGE_UIH] *= 1000.0; //convert sec to msec - //printf("x\t%d\t%g\tkTimeAfterStart\n", acquisitionTimesGE_UIH, d.CSA.sliceTiming[acquisitionTimesGE_UIH]); + //printf("\t%d\t%g\tTimeAfterStart(0021,1104)\n", d.imageNum, d.CSA.sliceTiming[acquisitionTimesGE_UIH]); acquisitionTimesGE_UIH++; break; case kICE_dims: { //issue568: LO (0021,1106) [X_4_1_1_1_1_160_1_1_1_1_1_277] @@ -5925,12 +6105,25 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); char iceStr[kDICOMStr]; dcmStr(lLength, &buffer[lPos], iceStr); dcmStrDigitsOnly(iceStr); - char *end; - int echo = (int)strtol(iceStr, &end, 10); + char *end, *echoStr; + //read the first item ('X' or numeric if uncombined) + char c = iceStr[0]; + if( c >= '0' && c <= '9' ){ + int coilNumber = (int)strtol(iceStr, &end, 10); + //if ((iceStr != end) && (coilNumber > 0) && (strlen(d.coilName) < 1)) { //nb with uncombined coil will still have a name, e.g. 'HeadNeck_64' + if ((iceStr != end) && (coilNumber > 0)) { + snprintf(d.coilName, kDICOMStr, "%d", coilNumber); + //printf("issue631 coil name '%s'\n", d.coilName); + d.coilCrc = mz_crc32X((unsigned char *)&d.coilName, strlen(d.coilName)); + } + } + //read the second item: the echo number ('4') + echoStr = strchr(iceStr, ' '); + int echo = (int)strtol(echoStr, &end, 10); //printMessage("%d:%d:'%s'\n", d.echoNum, echo, iceStr); if (iceStr != end) d.echoNum = echo; - //printMessage("%d:'%s'\n", echo, iceStr); + //printMessage("%d:'%s'\n", echo, echoStr); break; } case kPhaseEncodingDirectionPositiveSiemens: { @@ -6083,7 +6276,7 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); d.numberOfAverages = dcmStrFloat(lLength, &buffer[lPos]); break; case kImagingFrequency: - d.imagingFrequency = dcmStrFloat(lLength, &buffer[lPos]); + d.imagingFrequency = dcmStrDouble(lLength, &buffer[lPos]); break; case kTriggerTime: { if (prefs->isIgnoreTriggerTimes) @@ -6154,6 +6347,9 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); case kFlipAngle: d.flipAngle = dcmStrFloat(lLength, &buffer[lPos]); break; + case kVariableFlipAngleFlag: + d.isVariableFlipAngle = ('Y' == toupper(buffer[lPos])); //first character is either 'y'es or 'n'o + break; case kRadionuclideHalfLife: d.radionuclideHalfLife = dcmStrFloat(lLength, &buffer[lPos]); break; @@ -6229,6 +6425,12 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); } break; } + case kDeepLearningText: { + if ((d.manufacturer != kMANUFACTURER_SIEMENS) || (lLength < 2)) + break; + dcmStr(lLength, &buffer[lPos], d.deepLearningText, true); + break; + } case kAcquisitionMatrixText21: //fall through to kAcquisitionMatrixText case kAcquisitionMatrixText: { @@ -6305,6 +6507,8 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); d.decayFactor = dcmStrFloat(lLength, &buffer[lPos]); break; case kIconImageSequence: + if (lLength > 8) + break; //issue638: we will skip entire icon if there is an explicit length isIconImageSequence = true; if (sqDepthIcon < 0) sqDepthIcon = sqDepth; @@ -6316,6 +6520,10 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); //~d.numberOfDynamicScans = dcmStrInt(lLength, &buffer[lPos]); numberOfDynamicScans = dcmStrInt(lLength, &buffer[lPos]); break; + /*case kTRPhilips: + if (d.manufacturer != kMANUFACTURER_PHILIPS) break; + TRPhilips = dcmFloat(lLength, &buffer[lPos], d.isLittleEndian); + break;*/ case kMRAcquisitionType: //detect 3D acquisition: we can reorient these without worrying about slice time correct or BVEC/BVAL orientation if (lLength > 1) d.is2DAcq = (buffer[lPos] == '2') && (toupper(buffer[lPos + 1]) == 'D'); @@ -6345,7 +6553,10 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); break; //warp } case kScanningSequenceSiemens: - dcmStr(lLength, &buffer[lPos], scanningSequenceSiemens); + if (d.manufacturer == kMANUFACTURER_SIEMENS) + dcmStr(lLength, &buffer[lPos], scanningSequenceSiemens); + if (d.manufacturer == kMANUFACTURER_GE) //issue690 + diffusionDirectionTypeGE = dcmInt(lLength, &buffer[lPos], d.isLittleEndian); break; case kSequenceVariant21: if (d.manufacturer != kMANUFACTURER_SIEMENS) @@ -6486,7 +6697,8 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); // ((d.manufacturer == kMANUFACTURER_PHILIPS) && !is2005140FSQ)) && // (isAtFirstPatientPosition || isnan(d.patientPosition[1]))) //if((d.manufacturer == kMANUFACTURER_SIEMENS) || ((d.manufacturer == kMANUFACTURER_PHILIPS) && !is2005140FSQ)) - if ((d.manufacturer == kMANUFACTURER_MEDISO) || (d.manufacturer == kMANUFACTURER_TOSHIBA) || (d.manufacturer == kMANUFACTURER_CANON) || (d.manufacturer == kMANUFACTURER_HITACHI) || (d.manufacturer == kMANUFACTURER_SIEMENS) || (d.manufacturer == kMANUFACTURER_PHILIPS)) { + if (true) { + //if ((d.manufacturer == kMANUFACTURER_MEDISO) || (d.manufacturer == kMANUFACTURER_TOSHIBA) || (d.manufacturer == kMANUFACTURER_CANON) || (d.manufacturer == kMANUFACTURER_HITACHI) || (d.manufacturer == kMANUFACTURER_SIEMENS) || (d.manufacturer == kMANUFACTURER_PHILIPS)) { //for kMANUFACTURER_HITACHI see https://nciphub.org/groups/qindicom/wiki/StandardcompliantenhancedmultiframeDWI float v[4]; //dcmMultiFloat(lLength, (char*)&buffer[lPos], 3, v); @@ -6503,9 +6715,10 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); hasDwiDirectionality = true; d.isBVecWorldCoordinates = true; //e.g. Canon saved image space coordinates in Comments, world space in 0018, 9089 set_orientation0018_9089(&volDiffusion, lLength, &buffer[lPos], d.isLittleEndian); + isHasBVec = true; } break; - case kImagingFrequency2: + case kImagingFrequencyFD: d.imagingFrequency = dcmFloatDouble(lLength, &buffer[lPos], d.isLittleEndian); break; case kParallelReductionFactorOutOfPlane: @@ -6513,6 +6726,12 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); break; d.accelFactOOP = dcmFloatDouble(lLength, &buffer[lPos], d.isLittleEndian); break; + case kSARFD: + //Siemens XA uses kSARFD instead of kSAR + // ignore as we also need to know the definition issue 668 + //d.SAR = dcmFloatDouble(lLength, &buffer[lPos], d.isLittleEndian); + //maxSAR = fmax(maxSAR, d.SAR); + break; //case kFrameAcquisitionDuration : // frameAcquisitionDuration = dcmFloatDouble(lLength, &buffer[lPos], d.isLittleEndian); //issue369 // break; @@ -6537,42 +6756,7 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); break; //other manufacturers provide bvec directly, rather than bmatrix double bMat = dcmFloatDouble(lLength, &buffer[lPos], d.isLittleEndian); set_bMatrix(&volDiffusion, bMat, 0); - break; - } - case kDiffusionBValueXY: { - if (!(d.manufacturer == kMANUFACTURER_BRUKER)) - break; //other manufacturers provide bvec directly, rather than bmatrix - double bMat = dcmFloatDouble(lLength, &buffer[lPos], d.isLittleEndian); - set_bMatrix(&volDiffusion, bMat, 1); - break; - } - case kDiffusionBValueXZ: { - if (!(d.manufacturer == kMANUFACTURER_BRUKER)) - break; //other manufacturers provide bvec directly, rather than bmatrix - double bMat = dcmFloatDouble(lLength, &buffer[lPos], d.isLittleEndian); - set_bMatrix(&volDiffusion, bMat, 2); - break; - } - case kDiffusionBValueYY: { - if (!(d.manufacturer == kMANUFACTURER_BRUKER)) - break; //other manufacturers provide bvec directly, rather than bmatrix - double bMat = dcmFloatDouble(lLength, &buffer[lPos], d.isLittleEndian); - set_bMatrix(&volDiffusion, bMat, 3); - break; - } - case kDiffusionBValueYZ: { - if (!(d.manufacturer == kMANUFACTURER_BRUKER)) - break; //other manufacturers provide bvec directly, rather than bmatrix - double bMat = dcmFloatDouble(lLength, &buffer[lPos], d.isLittleEndian); - set_bMatrix(&volDiffusion, bMat, 4); - break; - } - case kDiffusionBValueZZ: { - if (!(d.manufacturer == kMANUFACTURER_BRUKER)) - break; //other manufacturers provide bvec directly, rather than bmatrix - double bMat = dcmFloatDouble(lLength, &buffer[lPos], d.isLittleEndian); - set_bMatrix(&volDiffusion, bMat, 5); - d.isVectorFromBMatrix = true; + isHasBMatrix = true; break; } case kSliceNumberMrPhilips: { @@ -6658,6 +6842,17 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); vFHPhilips = dcmFloat(lLength, &buffer[lPos], d.isLittleEndian); set_diffusion_directionPhilips(&volDiffusion, vFHPhilips, 2); break; + case kDeepLearningPhilips: { //CS + if (d.manufacturer != kMANUFACTURER_PHILIPS) + break; + char st[kDICOMStr]; + //see dcm_qa_cs_dl reports `none` or `CS_SENSE_AI` + dcmStr(lLength, &buffer[lPos], st); + if (strstr(st, "_AI") != NULL) { + d.isDeepLearning = true; + dcmStr(lLength, &buffer[lPos], d.deepLearningText, true); + } + } case kPrivatePerFrameSq: if (d.manufacturer != kMANUFACTURER_PHILIPS) break; @@ -6689,6 +6884,8 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); philMRImageDiffVolumeNumber = dcmStrInt(lLength, &buffer[lPos]); break; case kOriginalAttributesSq: + if (lLength > 8) + break; //issue639: we will skip entire icon if there is an explicit length is4000561SQ = true; break; case kWaveformSq: @@ -6703,9 +6900,14 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); case kCSAImageHeaderInfo: if ((lPos + lLength) > fileLen) break; - readCSAImageHeader(&buffer[lPos], lLength, &d.CSA, isVerbose, d.is3DAcq); //, dti4D); + readCSAImageHeader(&buffer[lPos], lLength, &d.CSA, isVerbose, d.is3DAcq); if (!d.isHasPhase) d.isHasPhase = d.CSA.isPhaseMap; + if ((d.CSA.coilNumber > 0) && (strlen(d.coilName) < 1)) { + snprintf(d.coilName, kDICOMStr, "%d", d.CSA.coilNumber); + //printf("issue631 coil name '%s'\n", d.coilName); + d.coilCrc = mz_crc32X((unsigned char *)&d.coilName, strlen(d.coilName)); + } break; case kCSASeriesHeaderInfo: if ((lPos + lLength) > fileLen) @@ -6744,6 +6946,11 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); break; d.shimGradientZ = dcmIntSS(lLength, &buffer[lPos], d.isLittleEndian); break; + case kVasCollapseFlagGE: //SS issue 690 16=DiffusionDtiDicomValue + if (d.manufacturer != kMANUFACTURER_GE) + break; + diffusionDirectionTypeGE = dcmIntSS(lLength, &buffer[lPos], d.isLittleEndian); + break; case kPrescanReuseString: //LO if (d.manufacturer != kMANUFACTURER_GE) break; @@ -6757,7 +6964,7 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); int isVerboseX = isVerbose; //for debugging only - in standard release we will enable user defined "isVerbose" //int isVerboseX = 2; if (isVerboseX > 1) - printMessage(" UserDefineDataGE file offset/length %ld %u\n", lFileOffset + lPos, lLength); + printMessage(" UserDefineDataGE file offset/length %zu %u\n", lFileOffset + lPos, lLength); if (lLength < 916) { //minimum size is hdr_offset=0, read 0x0394 printMessage(" GE header too small to be valid (A)\n"); break; @@ -6765,7 +6972,7 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); //debug code to export binary data /* char str[kDICOMStr]; - sprintf(str, "%s_ge.bin",fname); + snprintf(str, kDICOMStr, "%s_ge.bin",fname); FILE *pFile = fopen(str, "wb"); fwrite(&buffer[lPos], 1, lLength, pFile); fclose (pFile); @@ -6821,16 +7028,18 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); int sliceOrderFlag = dcmInt(2, (unsigned char *)hdr + kydir_off, true); if (isVerboseX > 1) printMessage(" GE phasePolarity/sliceOrder flags %d %d\n", phasePolarityFlag, sliceOrderFlag); - if (phasePolarityFlag == kGE_PHASE_ENCODING_POLARITY_FLIPPED) - d.phaseEncodingGE = kGE_PHASE_ENCODING_POLARITY_FLIPPED; - if (phasePolarityFlag == kGE_PHASE_ENCODING_POLARITY_UNFLIPPED) - d.phaseEncodingGE = kGE_PHASE_ENCODING_POLARITY_UNFLIPPED; - if (sliceOrderFlag == kGE_SLICE_ORDER_BOTTOM_UP) { - //https://cfmriweb.ucsd.edu/Howto/3T/operatingtips.html - if (d.phaseEncodingGE == kGE_PHASE_ENCODING_POLARITY_UNFLIPPED) + if (d.phaseEncodingGE == kGE_PHASE_ENCODING_POLARITY_UNKNOWN) { //issue674 precedence of 0018,9034 over 0043,102A + if (phasePolarityFlag == kGE_PHASE_ENCODING_POLARITY_FLIPPED) d.phaseEncodingGE = kGE_PHASE_ENCODING_POLARITY_FLIPPED; - else + if (phasePolarityFlag == kGE_PHASE_ENCODING_POLARITY_UNFLIPPED) d.phaseEncodingGE = kGE_PHASE_ENCODING_POLARITY_UNFLIPPED; + if (sliceOrderFlag == kGE_SLICE_ORDER_BOTTOM_UP) { + //https://cfmriweb.ucsd.edu/Howto/3T/operatingtips.html + if (d.phaseEncodingGE == kGE_PHASE_ENCODING_POLARITY_UNFLIPPED) + d.phaseEncodingGE = kGE_PHASE_ENCODING_POLARITY_FLIPPED; + else + d.phaseEncodingGE = kGE_PHASE_ENCODING_POLARITY_UNFLIPPED; + } } //if (sliceOrderFlag == kGE_SLICE_ORDER_TOP_DOWN) // d.sliceOrderGE = kGE_SLICE_ORDER_TOP_DOWN; @@ -6920,6 +7129,24 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); d.CSA.multiBandFactor = mb; break; } + case kCompressedSensingParameters: { //LO issue672 + if ((d.manufacturer != kMANUFACTURER_GE) || (lLength < 2)) + break; + //0043,10b7) LO [1.24\1\10\0] # 12, 4 Compressed Sensing Parameters + float cs = dcmStrFloat(lLength, &buffer[lPos]); + if (cs > 1.0) + d.compressedSensingFactor = cs; + //dcmStr(lLength, &buffer[lPos], d.compressedSensingText); + break; + } + case kDeepLearningParameters: { //LO issue672 + if ((d.manufacturer != kMANUFACTURER_GE) || (lLength < 2)) + break; + //(0043,10ca) LO [0.75\High] + d.isDeepLearning = true; + dcmStr(lLength, &buffer[lPos], d.deepLearningText, true); + break; + } case kGeiisFlag: if ((lLength > 4) && (buffer[lPos] == 'G') && (buffer[lPos + 1] == 'E') && (buffer[lPos + 2] == 'I') && (buffer[lPos + 3] == 'I')) { //read a few digits, as bug is specific to GEIIS, while GEMS are fine @@ -7070,30 +7297,30 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); // this section will report very little for implicit data //if (d.isHasReal) printf("r");else printf("m"); char str[kDICOMStr]; - sprintf(str, "%*c%04x,%04x %u@%ld ", sqDepth + 1, ' ', groupElement & 65535, groupElement >> 16, lLength, lFileOffset + lPos); + snprintf(str, kDICOMStr, "%*c%04x,%04x %u@%zu ", sqDepth + 1, ' ', groupElement & 65535, groupElement >> 16, lLength, lFileOffset + lPos); bool isStr = false; if (d.isExplicitVR) { - //sprintf(str, "%s%c%c ", str, vr[0], vr[1]); + //snprintf(str, kDICOMStr, "%s%c%c ", str, vr[0], vr[1]); //if (snprintf(str2, kDICOMStr-1, "%s%c%c", str, vr[0], vr[1]) < 0) exit(EXIT_FAILURE); strncat(str, &vr[0], 1); str[kDICOMStr-1] = '\0'; //silence warning -Wstringop-truncation strncat(str, &vr[1], 1); str[kDICOMStr-1] = '\0'; //silence warning -Wstringop-truncation strcat(str, " "); - //sprintf(str, "%s%c%c ", str2, vr[0], vr[1]); + //snprintf(str, kDICOMStr, "%s%c%c ", str2, vr[0], vr[1]); char str2[kDICOMStr] = ""; if ((vr[0] == 'F') && (vr[1] == 'D')) - sprintf(str2, "%g ", dcmFloatDouble(lLength, &buffer[lPos], d.isLittleEndian)); + snprintf(str2, kDICOMStr, "%g ", dcmFloatDouble(lLength, &buffer[lPos], d.isLittleEndian)); if ((vr[0] == 'F') && (vr[1] == 'L')) - sprintf(str2, "%g ", dcmFloat(lLength, &buffer[lPos], d.isLittleEndian)); + snprintf(str2, kDICOMStr, "%g ", dcmFloat(lLength, &buffer[lPos], d.isLittleEndian)); if ((vr[0] == 'S') && (vr[1] == 'S')) - sprintf(str2, "%d ", dcmInt(lLength, &buffer[lPos], d.isLittleEndian)); + snprintf(str2, kDICOMStr, "%d ", dcmInt(lLength, &buffer[lPos], d.isLittleEndian)); if ((vr[0] == 'S') && (vr[1] == 'L')) - sprintf(str2, "%d ", dcmInt(lLength, &buffer[lPos], d.isLittleEndian)); + snprintf(str2, kDICOMStr, "%d ", dcmInt(lLength, &buffer[lPos], d.isLittleEndian)); if ((vr[0] == 'U') && (vr[1] == 'S')) - sprintf(str2, "%d ", dcmInt(lLength, &buffer[lPos], d.isLittleEndian)); + snprintf(str2, kDICOMStr, "%d ", dcmInt(lLength, &buffer[lPos], d.isLittleEndian)); if ((vr[0] == 'U') && (vr[1] == 'L')) - sprintf(str, "%d ", dcmInt(lLength, &buffer[lPos], d.isLittleEndian)); + snprintf(str2, kDICOMStr, "%d ", dcmInt(lLength, &buffer[lPos], d.isLittleEndian)); if ((vr[0] == 'A') && (vr[1] == 'E')) isStr = true; if ((vr[0] == 'A') && (vr[1] == 'S')) @@ -7171,6 +7398,15 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); //Uncompressed data (unencapsulated) is sent in DICOM as a series of raw bytes or words (little or big endian) in the Value field of the Pixel Data element (7FE0,0010). Encapsulated data on the other hand is sent not as raw bytes or words but as Fragments contained in Items that are the Value field of Pixel Data printWarning("DICOM violation (contact vendor): compressed image without image fragments, assuming image offset defined by 0x7FE0,x0010: %s\n", fname); d.imageStart = encapsulatedDataImageStart; + } else if ((!isEncapsulatedData) && (d.imageStart < 128)) { + //issue639 d.samplesPerPixel == 3 + int imageStart = (int) (lPos- lLength); + int imgBytes = (int) (lLength); + int imgBytesExpected = (d.bitsAllocated >> 3) * d.samplesPerPixel * d.xyzDim[1] * d.xyzDim[2] ; + if ((imgBytes >= imgBytesExpected) && (d.xyzDim[1] > 1) && (d.xyzDim[2] > 1)) { + printf("Assuming final tag is Pixel Data (7fe0,0010) (issue 639)\n"); + d.imageStart = imageStart; + } } if ((d.manufacturer == kMANUFACTURER_GE) && (d.groupDelay > 0.0)) d.TR += d.groupDelay; //Strangely, for GE the sample rate is (0018,0080) + ((0043,107c) * 1000.0) @@ -7187,7 +7423,6 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); d.isHasOverlay = false; } //Recent Philips images include DateTime (0008,002A) but not separate date and time (0008,0022 and 0008,0032) -#define kYYYYMMDDlen 8 //how many characters to encode year,month,day in "YYYYDDMM" format if ((strlen(acquisitionDateTimeTxt) > (kYYYYMMDDlen + 5)) && (!isFloatDiff(d.acquisitionTime, 0.0f)) && (!isFloatDiff(d.acquisitionDate, 0.0f))) { // 20161117131643.80000 -> date 20161117 time 131643.80000 //printMessage("acquisitionDateTime %s\n",acquisitionDateTimeTxt); @@ -7236,7 +7471,7 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); d.xyzMM[3] = d.zSpacing; //use zSpacing if provided: depending on vendor, kZThick may or may not include a slice gap //printMessage("patientPositions = %d XYZT = %d slicePerVol = %d numberOfDynamicScans %d\n",patientPositionNum,d.xyzDim[3], d.locationsInAcquisition, d.numberOfDynamicScans); if ((d.manufacturer == kMANUFACTURER_PHILIPS) && (patientPositionNum > d.xyzDim[3])) { - d.CSA.numDti = d.xyzDim[3]; //issue506 + d.CSA.numDti = d.xyzDim[3]; //issue506 printMessage("Please check slice thicknesses: Philips R3.2.2 bug can disrupt estimation (%d positions reported for %d slices)\n", patientPositionNum, d.xyzDim[3]); //Philips reported different positions for each slice! } if ((d.imageStart > 144) && (d.xyzDim[1] > 1) && (d.xyzDim[2] > 1)) @@ -7270,6 +7505,8 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); d.isDerived = true; //to my knowledge, palette images always derived printWarning("Photometric Interpretation 'PALETTE COLOR' not supported\n"); } + if ((isHasBMatrix) && (!isHasBVec)) + printWarning("Underspecified BMatrix without BVector (issue 265)\n"); if ((d.compressionScheme == kCompress50) && (d.bitsAllocated > 8)) { //dcmcjpg with +ee can create .51 syntax images that are 8,12,16,24-bit: we can only decode 8/24-bit printError("Unable to decode %d-bit images with Transfer Syntax 1.2.840.10008.1.2.4.51, decompress with dcmdjpg or gdcmconv\n", d.bitsAllocated); @@ -7307,6 +7544,8 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); strcpy(d.protocolName, d.sequenceName); //protocolName (0018,1030) optional, sequence name (0018,0024) is not a good substitute for Siemens as it can vary per volume: *ep_b0 *ep_b1000#1, *ep_b1000#2, etc https://www.nitrc.org/forum/forum.php?thread_id=8771&forum_id=4703 if (numberOfFrames == 0) numberOfFrames = d.xyzDim[3]; + if ((numberOfDynamicScans < 1) && (maxTemporalPositionIndex > minTemporalPositionIndex)) + numberOfDynamicScans = maxTemporalPositionIndex - minTemporalPositionIndex + 1; if ((locationsInAcquisitionPhilips > 0) && ((d.xyzDim[3] % locationsInAcquisitionPhilips) == 0)) { d.xyzDim[4] = d.xyzDim[3] / locationsInAcquisitionPhilips; d.xyzDim[3] = locationsInAcquisitionPhilips; @@ -7333,7 +7572,7 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); if ((B0Philips >= 0) && (d.CSA.numDti == 0)) { d.CSA.dtiV[0] = B0Philips; d.CSA.numDti = 1; - } //issue409 Siemens XA saved as classic 2D not enhanced + } //issue409 Siemens XA saved as classic 2D not enhanced if (!isnan(patientPositionStartPhilips[1])) //for Philips data without for (int k = 0; k < 4; k++) d.patientPosition[k] = patientPositionStartPhilips[k]; @@ -7562,7 +7801,7 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); d.imageNum = abs((int)d.instanceUidCrc) % 2147483647; //INT_MAX; if (d.imageNum == 0) d.imageNum = 1; //https://github.com/rordenlab/dcm2niix/issues/341 - //d.imageNum = 1; //not set + //d.imageNum = 1; //not set } if ((numDimensionIndexValues < 1) && (d.manufacturer == kMANUFACTURER_PHILIPS) && (d.seriesNum > 99999) && (philMRImageDiffBValueNumber > 0)) { //Ugly kludge to distinguish Philips classic DICOM dti @@ -7578,10 +7817,14 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); //if ((d.manufacturer == kMANUFACTURER_SIEMENS) && (strcmp(d.sequenceName, "fldyn3d1")== 0)) { if ((d.manufacturer == kMANUFACTURER_SIEMENS) && (strstr(d.sequenceName, "fldyn3d1") != NULL)) { //combine DCE series https://github.com/rordenlab/dcm2niix/issues/252 - d.isStackableSeries = true; - d.imageNum += (d.seriesNum * 1000); - strcpy(d.seriesInstanceUID, d.studyInstanceUID); - d.seriesUidCrc = mz_crc32X((unsigned char *)&d.protocolName, strlen(d.protocolName)); + if (d.isXA10A) //issue689 + d.imageNum += (d.acquNum * 1000); + else { + d.isStackableSeries = true; + d.imageNum += (d.seriesNum * 1000); + strcpy(d.seriesInstanceUID, d.studyInstanceUID); + d.seriesUidCrc = mz_crc32X((unsigned char *)&d.protocolName, strlen(d.protocolName)); + } } //TODO533: alias Philips ASL PLD as frameDuration? isKludgeIssue533 //if ((d.manufacturer == kMANUFACTURER_PHILIPS) && ((!isTriggerSynced) || (!isProspectiveSynced)) ) //issue408 @@ -7600,7 +7843,7 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); } if (((d.manufacturer == kMANUFACTURER_TOSHIBA) || (d.manufacturer == kMANUFACTURER_CANON)) && (B0Philips > 0.0)) { //issue 388 char txt[1024] = {""}; - sprintf(txt, "b=%d(", (int)round(B0Philips)); + snprintf(txt, 1024, "b=%d(", (int)round(B0Philips)); if (strstr(d.imageComments, txt) != NULL) { //printf("%s>>>%s %g\n", txt, d.imageComments, B0Philips); int len = strlen(txt); @@ -7644,6 +7887,41 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); if ((d.manufacturer == kMANUFACTURER_SIEMENS) && (strstr(d.sequenceName, "fl2d1") != NULL)) { d.isLocalizer = true; } + // detect GE diffusion gradient cycling mode (see issue 635) + // GE diffusion epi + if ((d.epiVersionGE == kGE_EPI_EPI2) || (d.internalepiVersionGE == 2)) { + // Diffusion tensor file number + d.tensorFileGE = userData11GE; + // cycling systems: Premier, UHP, 7.0T + if ((strstr(d.manufacturersModelName, "Premier") != NULL) || (strstr(d.manufacturersModelName, "UHP") != NULL) || (strstr(d.manufacturersModelName, "7.0T") != NULL)) { + // cycling special OFF mode + if (isSameFloatGE(userData15GE, 0.72)) + d.diffCyclingModeGE = kGE_DIFF_CYCLING_SPOFF; + // 2TR cycling mode + else if (userData12GE == 2) { + d.diffCyclingModeGE = kGE_DIFF_CYCLING_2TR; + if (userData11GE == 0) + d.tensorFileGE = 2; + } + // 3TR cycling mode + else if (userData12GE == 3) { + d.diffCyclingModeGE = kGE_DIFF_CYCLING_3TR; + if (userData11GE == 0) + d.tensorFileGE = 3; + } + // (Default) ALLTR cycling mode + else + d.diffCyclingModeGE = kGE_DIFF_CYCLING_ALLTR; + } + // Non-cylcing systems: all other systems including MR750, Architect, etc + else { + d.diffCyclingModeGE = kGE_DIFF_CYCLING_OFF; + } + } + if ((d.accelFactPE < accelFactPE) && (accelFactPE > 1.0)) { + d.accelFactPE = accelFactPE; + //printf("Determining accelFactPE from 0018,9069 not 0021,1009 or 0051,1011\n"); + } //detect pepolar https://github.com/nipy/heudiconv/issues/479 if ((d.epiVersionGE == kGE_EPI_PEPOLAR_FWD) && (userData12GE == 1)) d.epiVersionGE = kGE_EPI_PEPOLAR_REV; @@ -7700,6 +7978,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 } + //issue690 + if ((d.manufacturer == kMANUFACTURER_GE) && (diffusionDirectionTypeGE > 0) && (diffusionDirectionTypeGE != 16)) + d.numberOfDiffusionDirectionGE = 0; //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"); @@ -7718,6 +7999,8 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); d.rawDataRunNumber = philMRImageDiffVolumeNumber; d.phaseNumber = 0; } + //issue 668: several SAR levels for different regions (IEC_HEAD, IEC_LOCAL, etc) + d.SAR = fmax(maxSAR, d.SAR); // 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 2458f75a..aa86ef41 100644 --- a/console/nii_dicom.h +++ b/console/nii_dicom.h @@ -50,11 +50,16 @@ extern "C" { #define kCPUsuf " " //unknown CPU #endif -#define kDCMdate "v1.0.20220720" +#define kDCMdate "v1.0.20230411" #define kDCMvers kDCMdate " " kJP2suf kLSsuf kCCsuf kCPUsuf static const int kMaxEPI3D = 1024; //maximum number of EPI images in Siemens Mosaic -static const int kMaxSlice2D = 65535; //issue460 maximum number of 2D slices in 4D (Philips) images + +#if defined(__linux__) //Unix users must use setrlimit + static const int kMaxSlice2D = 65535; //issue460 maximum number of 2D slices in 4D (Philips) images +#else + static const int kMaxSlice2D = 131070;// 65535; //issue460 maximum number of 2D slices in 4D (Philips) images +#endif static const int kMaxDTI4D = kMaxSlice2D; //issue460: maximum number of DTI directions for 4D (Philips) images, also maximum number of 2D slices for Enhanced DICOM and PAR/REC #define kDICOMStr 66 //64 characters plus NULL https://github.com/rordenlab/dcm2niix/issues/268 @@ -70,6 +75,8 @@ static const int kMaxDTI4D = kMaxSlice2D; //issue460: maximum number of DTI dire #define kMANUFACTURER_HITACHI 7 #define kMANUFACTURER_CANON 8 #define kMANUFACTURER_MEDISO 9 +#define kMANUFACTURER_MRSOLUTIONS 10 +#define kMANUFACTURER_HYPERFINE 11 //note: note a complete modality list, e.g. XA,PX, etc #define kMODALITY_UNKNOWN 0 @@ -98,6 +105,13 @@ static const int kMaxDTI4D = kMaxSlice2D; //issue460: maximum number of DTI dire #define kGE_EPI_PEPOLAR_REV_FWD_FLIP 7 #define kGE_EPI_PEPOLAR_FWD_REV_FLIP 8 +//GE Diff Gradient Cycling Mode +#define kGE_DIFF_CYCLING_UNKNOWN -1 +#define kGE_DIFF_CYCLING_OFF 0 +#define kGE_DIFF_CYCLING_ALLTR 1 +#define kGE_DIFF_CYCLING_2TR 2 +#define kGE_DIFF_CYCLING_3TR 3 +#define kGE_DIFF_CYCLING_SPOFF 100 //GE phase encoding #define kGE_PHASE_ENCODING_POLARITY_UNKNOWN -1 @@ -213,29 +227,28 @@ static const uint8_t MAX_NUMBER_OF_DIMENSIONS = 8; } TCSAitem; //Siemens csa item structure #endif struct TCSAdata { - float sliceTiming[kMaxEPI3D], dtiV[4], sliceNormV[4], bandwidthPerPixelPhaseEncode, sliceMeasurementDuration; - int numDti, SeriesHeader_offset, SeriesHeader_length, multiBandFactor, sliceOrder, slice_start, slice_end, mosaicSlices, protocolSliceNumber1, phaseEncodingDirectionPositive; - bool isPhaseMap; - + float sliceTiming[kMaxEPI3D], dtiV[4], sliceNormV[4], bandwidthPerPixelPhaseEncode, sliceMeasurementDuration; + int coilNumber, numDti, SeriesHeader_offset, SeriesHeader_length, multiBandFactor, sliceOrder, slice_start, slice_end, mosaicSlices, protocolSliceNumber1, phaseEncodingDirectionPositive; + bool isPhaseMap; }; struct TDICOMdata { long seriesNum; int xyzDim[5]; uint32_t coilCrc, seriesUidCrc, instanceUidCrc; int overlayStart[kMaxOverlay]; - int postLabelDelay, shimGradientX, shimGradientY, shimGradientZ, phaseNumber, spoiling, mtState, partialFourierDirection, interp3D, aslFlags, durationLabelPulseGE, epiVersionGE, internalepiVersionGE, maxEchoNumGE, rawDataRunNumber, numberOfImagesInGridUIH, numberOfDiffusionDirectionGE, phaseEncodingGE, protocolBlockStartGE, protocolBlockLengthGE, modality, dwellTime, effectiveEchoSpacingGE, phaseEncodingLines, phaseEncodingSteps, echoTrainLength, echoNum, sliceOrient, manufacturer, converted2NII, acquNum, imageNum, imageStart, imageBytes, bitsStored, bitsAllocated, samplesPerPixel,locationsInAcquisition, locationsInAcquisitionConflict, compressionScheme; - float xRayTubeCurrent, exposureTimeMs, numberOfExcitations, numberOfArms, numberOfPointsPerArm, groupDelay, decayFactor, percentSampling,waterFatShift, numberOfAverages, imagingFrequency, patientWeight, zSpacing, zThick, pixelBandwidth, SAR, phaseFieldofView, accelFactPE, accelFactOOP, flipAngle, fieldStrength, TE, TI, TR, intenScale, intenIntercept, intenScalePhilips, gantryTilt, lastScanLoc, angulation[4], velocityEncodeScaleGE; + int postLabelDelay, shimGradientX, shimGradientY, shimGradientZ, phaseNumber, spoiling, mtState, partialFourierDirection, interp3D, aslFlags, durationLabelPulseGE, epiVersionGE, internalepiVersionGE, maxEchoNumGE, rawDataRunNumber, numberOfImagesInGridUIH, numberOfDiffusionT2GE, numberOfDiffusionDirectionGE, tensorFileGE, diffCyclingModeGE, phaseEncodingGE, protocolBlockStartGE, protocolBlockLengthGE, modality, dwellTime, effectiveEchoSpacingGE, phaseEncodingLines, phaseEncodingSteps, frequencyEncodingSteps, phaseEncodingStepsOutOfPlane, echoTrainLength, echoNum, sliceOrient, manufacturer, converted2NII, acquNum, imageNum, imageStart, imageBytes, bitsStored, bitsAllocated, samplesPerPixel,locationsInAcquisition, locationsInAcquisitionConflict, compressionScheme; + float compressedSensingFactor, xRayTubeCurrent, exposureTimeMs, numberOfExcitations, numberOfArms, numberOfPointsPerArm, groupDelay, decayFactor, percentSampling,waterFatShift, numberOfAverages, patientWeight, zSpacing, zThick, pixelBandwidth, SAR, phaseFieldofView, accelFactPE, accelFactOOP, flipAngle, fieldStrength, TE, TI, TR, intenScale, intenIntercept, intenScalePhilips, gantryTilt, lastScanLoc, angulation[4], velocityEncodeScaleGE; float orient[7], patientPosition[4], patientPositionLast[4], xyzMM[4], stackOffcentre[4]; float rtia_timerGE, radionuclidePositronFraction, radionuclideTotalDose, radionuclideHalfLife, doseCalibrationFactor; //PET ISOTOPE MODULE ATTRIBUTES (C.8-57) float frameReferenceTime, frameDuration, ecat_isotope_halflife, ecat_dosage; float pixelPaddingValue; // used for both FloatPixelPaddingValue (0028, 0122) and PixelPaddingValue (0028, 0120); NaN if not present. - double acquisitionDuration, triggerDelayTime, RWVScale, RWVIntercept, dateTime, acquisitionTime, acquisitionDate, bandwidthPerPixelPhaseEncode; + double imagingFrequency, acquisitionDuration, triggerDelayTime, RWVScale, RWVIntercept, dateTime, acquisitionTime, acquisitionDate, bandwidthPerPixelPhaseEncode; char parallelAcquisitionTechnique[kDICOMStr], radiopharmaceutical[kDICOMStr], convolutionKernel[kDICOMStr], unitsPT[kDICOMStr], decayCorrection[kDICOMStr], attenuationCorrectionMethod[kDICOMStr],reconstructionMethod[kDICOMStr]; char prescanReuseString[kDICOMStr], imageOrientationText[kDICOMStr], pulseSequenceName[kDICOMStr], coilElements[kDICOMStr], coilName[kDICOMStr], phaseEncodingDirectionDisplayedUIH[kDICOMStr], imageBaseName[kDICOMStr], stationName[kDICOMStr], softwareVersions[kDICOMStr], deviceSerialNumber[kDICOMStr], institutionName[kDICOMStr], referringPhysicianName[kDICOMStr], instanceUID[kDICOMStr], seriesInstanceUID[kDICOMStr], studyInstanceUID[kDICOMStr], bodyPartExamined[kDICOMStr], procedureStepDescription[kDICOMStr], imageTypeText[kDICOMStr], imageType[kDICOMStr], institutionalDepartmentName[kDICOMStr], manufacturersModelName[kDICOMStr], patientID[kDICOMStr], patientOrient[kDICOMStr], patientName[kDICOMStr], accessionNumber[kDICOMStr], seriesDescription[kDICOMStr], studyID[kDICOMStr], sequenceName[kDICOMStr], protocolName[kDICOMStr],sequenceVariant[kDICOMStr],scanningSequence[kDICOMStr], patientBirthDate[kDICOMStr], patientAge[kDICOMStr], studyDate[kDICOMStr],studyTime[kDICOMStr]; - char scanOptions[kDICOMStrLarge], institutionAddress[kDICOMStrLarge], imageComments[kDICOMStrLarge]; + char deepLearningText[kDICOMStrLarge], scanOptions[kDICOMStrLarge], institutionAddress[kDICOMStrLarge], imageComments[kDICOMStrLarge]; uint32_t dimensionIndexValues[MAX_NUMBER_OF_DIMENSIONS]; struct TCSAdata CSA; - bool isRealIsPhaseMapHz, isPrivateCreatorRemap, isHasOverlay, isEPI, isIR, isPartialFourier, isDiffusion, isVectorFromBMatrix, isRawDataStorage, isGrayscaleSoftcopyPresentationState, isStackableSeries, isCoilVaries, isNonParallelSlices, isBVecWorldCoordinates, isSegamiOasis, isXA10A, isScaleOrTEVaries, isScaleVariesEnh, isDerived, isXRay, isMultiEcho, isValid, is3DAcq, is2DAcq, isExplicitVR, isLittleEndian, isPlanarRGB, isSigned, isHasPhase, isHasImaginary, isHasReal, isHasMagnitude,isHasMixed, isFloat, isResampled, isLocalizer; + bool isDeepLearning, isVariableFlipAngle, isQuadruped, isRealIsPhaseMapHz, isPrivateCreatorRemap, isHasOverlay, isEPI, isIR, isPartialFourier, isDiffusion, isVectorFromBMatrix, isRawDataStorage, isGrayscaleSoftcopyPresentationState, isStackableSeries, isCoilVaries, isNonParallelSlices, isBVecWorldCoordinates, isSegamiOasis, isXA10A, isScaleOrTEVaries, isScaleVariesEnh, isDerived, isXRay, isMultiEcho, isValid, is3DAcq, is2DAcq, isExplicitVR, isLittleEndian, isPlanarRGB, isSigned, isHasPhase, isHasImaginary, isHasReal, isHasMagnitude,isHasMixed, isFloat, isResampled, isLocalizer; char phaseEncodingRC, patientSex; }; struct TDCMprefs { diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index 5cfb3ad9..f913f2ff 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -59,6 +59,7 @@ #ifndef M_PI #define M_PI 3.14159265358979323846 #endif +#define kSliceTolerance 0.2 #if defined(_WIN64) || defined(_WIN32) const char kPathSeparator = '\\'; @@ -390,7 +391,7 @@ void siemensPhilipsCorrectBvecs(struct TDICOMdata *d, int sliceDir, struct TDTI for (int i = 0; i < d->CSA.numDti; i++) { float vLen = sqrt((vx[i].V[1] * vx[i].V[1]) + (vx[i].V[2] * vx[i].V[2]) + (vx[i].V[3] * vx[i].V[3])); if ((vx[i].V[0] <= FLT_EPSILON) || (vLen <= FLT_EPSILON)) { //bvalue=0 - if (vx[i].V[0] > 5.0) //Philip stores n.b. UIH B=1.25126 Vec=0,0,0 while Philips stored isotropic images + if ((vx[i].V[0] > 50.0) && (!d->isDerived)) //Philip stores n.b. UIH B=1.25126 Vec=0,0,0 while Philips stored isotropic images printWarning("Volume %d appears to be derived image ADC/Isotropic (non-zero b-value with zero vector length)\n", i); continue; //do not normalize or reorient b0 vectors } //if bvalue=0 @@ -629,6 +630,10 @@ int phoenixOffsetCSASeriesHeader(unsigned char *buff, int lLength) { memcpy(&tagCSA, &buff[lPos], sizeof(tagCSA)); //read tag if (!littleEndianPlatform()) nifti_swap_4bytes(1, &tagCSA.nitems); + if (tagCSA.nitems > 128) { + printError("%d n_tags CSA Series Header corrupted (0029,1020 ) see issue 633.\n", tagCSA.nitems); + return EXIT_FAILURE; + } //printf("%d CSA of %s %d\n",lPos, tagCSA.name, tagCSA.nitems); lPos += sizeof(tagCSA); if (strcmp(tagCSA.name, "MrPhoenixProtocol") == 0) @@ -646,7 +651,7 @@ int phoenixOffsetCSASeriesHeader(unsigned char *buff, int lLength) { #define kMaxWipFree 64 typedef struct { - float TE0, TE1, delayTimeInTR, phaseOversampling, phaseResolution, txRefAmp; + float TE0, TE1, delayTimeInTR, phaseOversampling, phaseResolution, txRefAmp, accelFactTotal; int phaseEncodingLines, existUcImageNumb, ucMode, baseResolution, interp, partialFourier, echoSpacing, difBipolar, parallelReductionFactorInPlane, refLinesPE, combineMode, patMode, ucMTC, accelFact3D; float alFree[kMaxWipFree]; @@ -676,6 +681,7 @@ void siemensCsaAscii(const char *filename, TCsaAscii *csaAscii, int csaOffset, i csaAscii->difBipolar = 0; //0=not assigned,1=bipolar,2=monopolar csaAscii->parallelReductionFactorInPlane = 0; csaAscii->accelFact3D = 0;//lAccelFact3D + csaAscii->accelFactTotal = 0.0; csaAscii->refLinesPE = 0; csaAscii->combineMode = 0; csaAscii->patMode = 0; @@ -707,8 +713,11 @@ void siemensCsaAscii(const char *filename, TCsaAscii *csaAscii, int csaOffset, i size_t result = fread(buffer, 1, csaLength, pFile); if ((int)result != csaLength) return; + fclose(pFile); //next bit complicated: restrict to ASCII portion to avoid buffer overflow errors in BINARY portion int startAscii = phoenixOffsetCSASeriesHeader((unsigned char *)buffer, csaLength); + //n.b. previous function parses binary V* "SV10" portion of header + // it will return "EXIT_FAILURE for text based X* "" int csaLengthTrim = csaLength; char *bufferTrim = buffer; if ((startAscii > 0) && (startAscii < csaLengthTrim)) { //ignore binary data at start @@ -757,6 +766,12 @@ void siemensCsaAscii(const char *filename, TCsaAscii *csaAscii, int csaOffset, i csaAscii->parallelReductionFactorInPlane = readKey(keyStrAF, keyPos, csaLengthTrim); char keyStrAF3D[] = "sPat.lAccelFact3D"; csaAscii->accelFact3D = readKey(keyStrAF3D, keyPos, csaLengthTrim); + char keyStrAFTotal[] = "sPat.dTotalAccelFact"; + csaAscii->accelFactTotal = readKeyFloat(keyStrAFTotal, keyPos, csaLengthTrim); + //issue 672: the tag "sSliceAcceleration.lMultiBandFactor" is not reliable: + // series 7 dcm_qa_xa30 has x3 multiband, but this tag reports "1" (perhaps cmrr sequences) + //char keyStrMB[] = "sSliceAcceleration.lMultiBandFactor"; + //csaAscii->multiBandFactor = readKey(keyStrMB, keyPos, csaLengthTrim); char keyStrRef[] = "sPat.lRefLinesPE"; csaAscii->refLinesPE = readKey(keyStrRef, keyPos, csaLengthTrim); char keyStrCombineMode[] = "ucCoilCombineMode"; @@ -801,7 +816,7 @@ void siemensCsaAscii(const char *filename, TCsaAscii *csaAscii, int csaOffset, i if (keyPosTi) { for (int k = 0; k < kMaxWipFree; k++) { char txt[1024] = {""}; - sprintf(txt, "%s%d]", keyStrTiFree, k); + snprintf(txt, 1024, "%s%d]", keyStrTiFree, k); csaAscii->alTI[k] = readKeyFloatNan(txt, keyPos, csaLengthTrim); } } @@ -814,7 +829,7 @@ void siemensCsaAscii(const char *filename, TCsaAscii *csaAscii, int csaOffset, i if (keyPosFree) { for (int k = 0; k < kMaxWipFree; k++) { char txt[1024] = {""}; - sprintf(txt, "%s%d]", keyStrAlFree, k); + snprintf(txt, 1024, "%s%d]", keyStrAlFree, k); csaAscii->alFree[k] = readKeyFloat(txt, keyPos, csaLengthTrim); } } @@ -833,7 +848,7 @@ void siemensCsaAscii(const char *filename, TCsaAscii *csaAscii, int csaOffset, i if (keyPosFree) { for (int k = 0; k < kMaxWipFree; k++) { char txt[1024] = {""}; - sprintf(txt, "%s%d]", keyStrAdFree, k); + snprintf(txt, 1024, "%s%d]", keyStrAdFree, k); csaAscii->adFree[k] = readKeyFloatNan(txt, keyPos, csaLengthTrim); } } @@ -889,7 +904,6 @@ void siemensCsaAscii(const char *filename, TCsaAscii *csaAscii, int csaOffset, i char keyStrSh7[] = "sGRADSPEC.alShimCurrent[4]"; shimSetting[7] = readKeyFloat(keyStrSh7, keyPos, csaLengthTrim); } - fclose(pFile); free(buffer); return; } // siemensCsaAscii() @@ -1028,6 +1042,7 @@ void json_Str(FILE *fp, const char *sLabel, char *sVal) { // issue131,425 int o = 0; for (int i = 0; i < (int)strlen(sVal); i++) { //escape double quote (") and Backslash + //if ((sVal[i] == '"') || (sVal[i] == '\\') || (sVal[i] == '/')) { //issue640: escape double quotes, back slash, or slash if ((sVal[i] == '"') || (sVal[i] == '\\')) { //escape double quotes and back slash sValEsc[o] = '\\'; o++; @@ -1205,7 +1220,7 @@ tse3d: T2*/ //Imaging Frequency (0018,0084) can be useful https://support.brainvoyager.com/brainvoyager/functional-analysis-preparation/29-pre-processing/78-epi-distortion-correction-echo-spacing-and-bandwidth // however, UIH stores 128176031 not 128.176031 https://github.com/rordenlab/dcm2niix/issues/225 if (d.imagingFrequency < 9000000) - json_Float(fp, "\t\"ImagingFrequency\": %g,\n", d.imagingFrequency); + fprintf(fp, "\t\"ImagingFrequency\": %.10g,\n", d.imagingFrequency); switch (d.manufacturer) { case kMANUFACTURER_BRUKER: fprintf(fp, "\t\"Manufacturer\": \"Bruker\",\n"); @@ -1234,6 +1249,12 @@ tse3d: T2*/ case kMANUFACTURER_UIH: fprintf(fp, "\t\"Manufacturer\": \"UIH\",\n"); break; + case kMANUFACTURER_MRSOLUTIONS: + fprintf(fp, "\t\"Manufacturer\": \"MRSolutions\",\n"); + break; + case kMANUFACTURER_HYPERFINE: + fprintf(fp, "\t\"Manufacturer\": \"Hyperfine\",\n"); + break; }; //if (d.epiVersionGE == 0) // fprintf(fp, "\t\"PulseSequenceName\": \"epi\",\n"); @@ -1274,6 +1295,8 @@ tse3d: T2*/ //d.patientBirthDate //convert from DICOM YYYYMMDD to JSON //d.patientAge //4-digit Age String: nnnD, nnnW, nnnM, nnnY; } + if (d.isQuadruped) + json_Bool(fp, "\t\"Quadruped\": %s,\n", true); // BIDS suggests 0018,9020 but Siemens V-series do not populate this, alternatives are CSA or (0018,0021) CS [SK\MTC\SP] json_Str(fp, "\t\"BodyPartExamined\": \"%s\",\n", d.bodyPartExamined); json_Str(fp, "\t\"PatientPosition\": \"%s\",\n", d.patientOrient); // 0018,5100 = PatientPosition in DICOM json_Str(fp, "\t\"ProcedureStepDescription\": \"%s\",\n", d.procedureStepDescription); @@ -1332,7 +1355,6 @@ tse3d: T2*/ if ((strstr(d.imageTypeText, "_ND") != NULL) || (strstr(d.imageType, "_ND") != NULL) || (strstr(d.imageTypeText, "_ND") != NULL) || (strstr(d.imageType, "_ND") != NULL)) fprintf(fp, "\t\"NonlinearGradientCorrection\": false,\n"); - if (d.isDerived) //DICOM is derived image or non-spatial file (sounds, etc) fprintf(fp, "\t\"RawImage\": false,\n"); if (d.seriesNum > 0) @@ -1366,8 +1388,11 @@ tse3d: T2*/ // if (d.acquisitionDate > 0.0) fprintf(fp, "\t\"AcquisitionDate\": %8.0f,\n", d.acquisitionDate ); if (d.acquNum > 0) fprintf(fp, "\t\"AcquisitionNumber\": %d,\n", d.acquNum); - json_Str(fp, "\t\"ImageComments\": \"%s\",\n", d.imageComments); - json_Str(fp, "\t\"ConversionComments\": \"%s\",\n", opts.imageComments); + bool maskComments = (strlen(opts.imageComments) == 1) && (opts.imageComments[0] == '\t'); + if (!maskComments) { + json_Str(fp, "\t\"ImageComments\": \"%s\",\n", d.imageComments); + json_Str(fp, "\t\"ConversionComments\": \"%s\",\n", opts.imageComments); + } //if conditionals: the following values are required for DICOM MRI, but not available for CT json_Float(fp, "\t\"TriggerDelayTime\": %g,\n", d.triggerDelayTime); if (d.RWVScale != 0) { @@ -1461,6 +1486,7 @@ tse3d: T2*/ json_Float(fp, "\t\"SliceThickness\": %g,\n", d.zThick); json_Float(fp, "\t\"SpacingBetweenSlices\": %g,\n", d.zSpacing); } + //if (!opts.isAnonymizeBIDS) //issue668 is SAR identifiable?? json_Float(fp, "\t\"SAR\": %g,\n", d.SAR); if (d.numberOfAverages > 1.0) json_Float(fp, "\t\"NumberOfAverages\": %g,\n", d.numberOfAverages); @@ -1495,6 +1521,8 @@ tse3d: T2*/ fprintf(fp, "\t\"SpoilingType\": \"COMBINED\",\n"); json_Float(fp, "\t\"InversionTime\": %g,\n", d.TI / 1000.0); json_Float(fp, "\t\"FlipAngle\": %g,\n", d.flipAngle); + if (d.isVariableFlipAngle) + json_Bool(fp, "\t\"VariableFlipAngleFlag\": %s,\n", true); // BIDS suggests 0018,9020 but Siemens V-series do not populate this, alternatives are CSA or (0018,0021) CS [SK\MTC\SP] bool interp = false; //2D interpolation float phaseOversampling = 0.0; //n.b. https://neurostars.org/t/getting-missing-ge-information-required-by-bids-for-common-preprocessing/1357/7 @@ -1511,6 +1539,31 @@ tse3d: T2*/ json_Str(fp, "\t\"PrescanReuseString\": \"%s\",\n", d.prescanReuseString); float delayTimeInTR = -0.01; float repetitionTimePreparation = 0.0; + // GE Diffusion specific fields + if ((d.epiVersionGE == kGE_EPI_EPI2) || (d.internalepiVersionGE == 2)) { + if (d.numberOfDiffusionDirectionGE > 0) + fprintf(fp, "\t\"NumberOfDiffusionDirectionGE\": %d,\n", d.numberOfDiffusionDirectionGE); + if (d.numberOfDiffusionT2GE > 0) + fprintf(fp, "\t\"NumberOfDiffusionT2GE\": %d,\n", d.numberOfDiffusionT2GE); + if (d.tensorFileGE > 0) + fprintf(fp, "\t\"TensorFileNumberGE\": %d,\n", d.tensorFileGE); + if (opts.diffCyclingModeGE >= 0) { + fprintf(fp, "\t\"DiffGradientCyclingGE\": \"OVERRIDE\",\n"); // see issue 635 + d.diffCyclingModeGE = opts.diffCyclingModeGE; + } + if (d.diffCyclingModeGE > 0) { + if (d.diffCyclingModeGE == kGE_DIFF_CYCLING_OFF) + fprintf(fp, "\t\"DiffGradientCyclingGE\": \"OFF\",\n"); + if (d.diffCyclingModeGE == kGE_DIFF_CYCLING_ALLTR) + fprintf(fp, "\t\"DiffGradientCyclingGE\": \"ALLTR\",\n"); + if (d.diffCyclingModeGE == kGE_DIFF_CYCLING_2TR) + fprintf(fp, "\t\"DiffGradientCyclingGE\": \"2TR\",\n"); + if (d.diffCyclingModeGE == kGE_DIFF_CYCLING_3TR) + fprintf(fp, "\t\"DiffGradientCyclingGE\": \"3TR\",\n"); + if (d.diffCyclingModeGE == kGE_DIFF_CYCLING_SPOFF) + fprintf(fp, "\t\"DiffGradientCyclingGE\": \"SPOFF\",\n"); + } + } #ifdef myReadAsciiCsa if ((d.manufacturer == kMANUFACTURER_SIEMENS) && (d.CSA.SeriesHeader_offset > 0) && (d.CSA.SeriesHeader_length > 0)) { float pf = 1.0f; //partial fourier @@ -1564,7 +1617,7 @@ tse3d: T2*/ json_FloatNotNan(fp, "\t\"PostLabelDelay\": %g,\n", csaAscii.adFree[2] * (1.0 / 1000000.0)); //usec -> sec json_FloatNotNan(fp, "\t\"NumRFBlocks\": %g,\n", csaAscii.adFree[3]); json_FloatNotNan(fp, "\t\"RFGap\": %g,\n", csaAscii.adFree[4] * (1.0 / 1000000.0)); //usec -> sec - json_FloatNotNan(fp, "\t\"MeanGzx10\": %g,\n", csaAscii.adFree[10]); // mT/m + json_FloatNotNan(fp, "\t\"MeanGzx10\": %g,\n", csaAscii.adFree[10]); json_FloatNotNan(fp, "\t\"PhiAdjust\": %g,\n", csaAscii.adFree[11]); // percent } //ASL specific tags - 3D pCASL Danny J.J. Wang http://www.loft-lab.org @@ -1631,14 +1684,14 @@ tse3d: T2*/ for (int k = 11; k < 31; k++) { if (isValid) { char newstr[256]; - sprintf(newstr, "\t\"PLD%d\": %%g,\n", k-11); + snprintf(newstr, 256, "\t\"PLD%d\": %%g,\n", k-11); json_Float(fp, newstr, csaAscii.alFree[k]/ 1000.0); //ms -> sec if (csaAscii.alFree[k] <= 0.0) isValid = false; }//isValid } //for k */ for (int k = 3; k < 11; k++) { //vessel locations char newstr[256]; - sprintf(newstr, "\t\"sWipMemBlockAdFree%d\": %%g,\n", k); //issue483: sWipMemBlock.AdFree -> sWipMemBlockAdFree + snprintf(newstr, 256, "\t\"sWipMemBlockAdFree%d\": %%g,\n", k); //issue483: sWipMemBlock.AdFree -> sWipMemBlockAdFree json_FloatNotNan(fp, newstr, csaAscii.adFree[k]); } } @@ -1762,25 +1815,31 @@ tse3d: T2*/ if ((csaAscii.ucMTC == 1) && (d.mtState < 0)) //precedence for 0018,9020 over CSA json_Bool(fp, "\t\"MTState\": %s,\n", 1); json_Str(fp, "\t\"ConsistencyInfo\": \"%s\",\n", consistencyInfo); - if (csaAscii.accelFact3D > 1.01) json_Float(fp, "\t\"AccelFact3D\": %g,\n", csaAscii.accelFact3D); //see *spcR_44ns where "sPat.lAccelFactPE = 1", "sPat.lAccelFact3D = 2" (0051,1011) LO [p2], perhaps ParallelReductionFactorInPlane should be 1? + if (csaAscii.accelFact3D > 0) + d.accelFactOOP = csaAscii.accelFact3D; + //see issue 672 if (csaAscii.accelFact3D > 1.01) json_Float(fp, "\t\"AccelFact3D\": %g,\n", csaAscii.accelFact3D); //see *spcR_44ns where "sPat.lAccelFactPE = 1", "sPat.lAccelFact3D = 2" (0051,1011) LO [p2], perhaps ParallelReductionFactorInPlane should be 1? if (csaAscii.parallelReductionFactorInPlane > 0) { //AccelFactorPE -> phase encoding + //1=SENSE, 2=GRAPPA, 32=SMS??, 256=CompressedSense? if (csaAscii.patMode == 1) fprintf(fp, "\t\"MatrixCoilMode\": \"SENSE\",\n"); if (csaAscii.patMode == 2) fprintf(fp, "\t\"MatrixCoilMode\": \"GRAPPA\",\n"); - if (d.accelFactPE < 1.0) { //value not found in DICOM header, but WAS found in CSA ascii - d.accelFactPE = csaAscii.parallelReductionFactorInPlane; //value found in ASCII but not in DICOM (0051,1011) - } + d.accelFactPE = csaAscii.parallelReductionFactorInPlane; //issue672: csa precedence over value found in DICOM (0051,1011) if ((csaAscii.accelFact3D < 1.01) && (csaAscii.parallelReductionFactorInPlane != (int)(d.accelFactPE))) printWarning("ParallelReductionFactorInPlane reported in DICOM [0051,1011] (%d) does not match CSA series value %d\n", (int)(d.accelFactPE), csaAscii.parallelReductionFactorInPlane); } + if ((csaAscii.patMode == 256) && (!isnan(csaAscii.accelFactTotal)) && (csaAscii.accelFactTotal > (d.accelFactPE * d.accelFactOOP) )) + d.compressedSensingFactor = csaAscii.accelFactTotal; //see dcm_qa_cs_dl } else { //e.g. Siemens Vida does not have CSA header, but has many attributes json_Str(fp, "\t\"ReceiveCoilActiveElements\": \"%s\",\n", d.coilElements); if (strcmp(d.coilElements, d.coilName) != 0) json_Str(fp, "\t\"CoilString\": \"%s\",\n", d.coilName); - if ((d.manufacturer == kMANUFACTURER_SIEMENS) && (!d.is3DAcq) && (d.phaseEncodingLines > d.echoTrainLength) && (d.echoTrainLength > 1)) { + int phaseEncodingLines = d.phaseEncodingLines; + if (phaseEncodingLines < 1) //support enhanced DICOM terminology + phaseEncodingLines = d.phaseEncodingSteps; + if ((d.manufacturer == kMANUFACTURER_SIEMENS) && (!d.is3DAcq) && (phaseEncodingLines > d.echoTrainLength) && (d.echoTrainLength > 1)) { //ETL is > 1, as some GE files list 1, as an example see series mr_0005 in dcm_qa_nih - float pf = (float)d.phaseEncodingLines; + float pf = (float)phaseEncodingLines; if (d.accelFactPE > 1) pf = (float)pf / (float)d.accelFactPE; //estimate: not sure if we round up or down pf = (float)d.echoTrainLength / (float)pf; @@ -1838,7 +1897,7 @@ tse3d: T2*/ fprintf(fp, "\t\"MultibandAccelerationFactor\": %d,\n", d.CSA.multiBandFactor); json_Float(fp, "\t\"PercentPhaseFOV\": %g,\n", d.phaseFieldofView); json_Float(fp, "\t\"PercentSampling\": %g,\n", d.percentSampling); - if (d.echoTrainLength > 1) //>1 as for Siemens EPI this is 1, Siemens uses EPI factor http://mriquestions.com/echo-planar-imaging.html + if (d.echoTrainLength > 1) //>1 as for Siemens EPI this is 1, Siemens uses EPI factor http://mriquestions.com/echo-planar-imaging.html fprintf(fp, "\t\"EchoTrainLength\": %d,\n", d.echoTrainLength); //0018,0091 Combination of partial fourier and in-plane parallel imaging if (d.partialFourierDirection == kPARTIAL_FOURIER_DIRECTION_PHASE) fprintf(fp, "\t\"PartialFourierDirection\": \"PHASE\",\n"); @@ -1854,6 +1913,10 @@ tse3d: T2*/ fprintf(fp, "\t\"PhaseEncodingStepsNoPartialFourier\": %d,\n", d.phaseEncodingSteps); } else if (d.phaseEncodingSteps > 0) fprintf(fp, "\t\"PhaseEncodingSteps\": %d,\n", d.phaseEncodingSteps); + if (d.frequencyEncodingSteps > 0) + fprintf(fp, "\t\"FrequencyEncodingSteps\": %d,\n", d.frequencyEncodingSteps); + if (d.phaseEncodingStepsOutOfPlane > 0) + fprintf(fp, "\t\"PhaseEncodingStepsOutOfPlane\": %d,\n", d.phaseEncodingStepsOutOfPlane); if ((d.phaseEncodingLines > 0) && (d.modality == kMODALITY_MR)) fprintf(fp, "\t\"AcquisitionMatrixPE\": %d,\n", d.phaseEncodingLines); //Compute ReconMatrixPE @@ -1872,6 +1935,12 @@ tse3d: T2*/ } if ((d.modality == kMODALITY_MR) && (reconMatrixPE > 0)) fprintf(fp, "\t\"ReconMatrixPE\": %d,\n", reconMatrixPE); + if ((d.accelFactPE > 1.0) && (d.manufacturer == kMANUFACTURER_PHILIPS) && strstr(d.parallelAcquisitionTechnique, "CSENSE") ) { + //see dcm_qa_cs_dl: while GE allows you to set ASSET and compressed sense, Philips reports only CSENSE + d.compressedSensingFactor = d.accelFactPE; + d.accelFactPE = 1.0; + d.parallelAcquisitionTechnique[0] = '\0'; + } double bandwidthPerPixelPhaseEncode = d.bandwidthPerPixelPhaseEncode; if (bandwidthPerPixelPhaseEncode == 0.0) bandwidthPerPixelPhaseEncode = d.CSA.bandwidthPerPixelPhaseEncode; @@ -1882,7 +1951,19 @@ tse3d: T2*/ json_Str(fp, "\t\"ParallelAcquisitionTechnique\": \"%s\",\n", d.parallelAcquisitionTechnique); //https://github.com/rordenlab/dcm2niix/issues/314 if (d.accelFactOOP > 1.0) - fprintf(fp, "\t\"ParallelReductionOutOfPlane\": %g,\n", d.accelFactOOP); + json_Float(fp, "\t\"ParallelReductionFactorOutOfPlane\": %g,\n", d.accelFactOOP); //issue672 + if (d.compressedSensingFactor > 1.0) + json_Float(fp, "\t\"CompressedSensingFactor\": %g,\n", d.compressedSensingFactor); + //detect if Siemens data is DeepLearning: see dcm_qa_cs_dl + if (d.manufacturer == kMANUFACTURER_SIEMENS) { + //DRB,DRG,DRS DeepReveal Boost,Gain,Sharp + d.isDeepLearning = (strstr(d.imageType, "_DRB_")|| strstr(d.imageType, "_DRG_") || strstr(d.imageType, "_DRS_") || + strstr(d.imageTypeText, "_DRB_")|| strstr(d.imageTypeText, "_DRG_") || strstr(d.imageTypeText, "_DRS_")); + } + if (d.isDeepLearning) { + json_Bool(fp, "\t\"DeepLearning\": %s,\n", 1); + json_Str(fp, "\t\"DeepLearningDetails\": \"%s\",\n", d.deepLearningText); + } //EffectiveEchoSpacing // Siemens bandwidthPerPixelPhaseEncode already accounts for the effects of parallel imaging, // interpolation, phaseOversampling, and phaseResolution, in the context of the size of the @@ -2001,7 +2082,7 @@ tse3d: T2*/ } //only save PhaseEncodingDirection if BOTH direction and POLARITY are known //Slice Timing UIH or GE >>>> //in theory, we should also report XA10 slice times here, but see series 24 of https://github.com/rordenlab/dcm2niix/issues/236 - if ((d.modality != kMODALITY_CT) && (d.modality != kMODALITY_PT) && (!d.is3DAcq) && (d.CSA.sliceTiming[0] >= 0.0)) { + if ((d.modality != kMODALITY_CT) && (d.modality != kMODALITY_PT) && (!d.is3DAcq) && (h->dim[3] > 1) && (d.CSA.sliceTiming[1] >= 0.0) && (d.CSA.sliceTiming[0] >= 0.0)) { fprintf(fp, "\t\"SliceTiming\": [\n"); for (int i = 0; i < h->dim[3]; i++) { if (i != 0) @@ -2131,7 +2212,6 @@ unsigned char *reorderVolumes(struct nifti_1_header *hdr, unsigned char *inImg, for (int i = 0; i < numVol; i++) inPos[i] = i; unsigned char *tempVol = (unsigned char *)malloc(numVolBytes); - int outPos = 0; for (int o = 0; o < numVol; o++) { int i = inPos[volOrderIndex[o]]; //input volume if (i == o) @@ -2140,7 +2220,6 @@ unsigned char *reorderVolumes(struct nifti_1_header *hdr, unsigned char *inImg, memcpy(&inImg[o * numVolBytes], &inImg[i * numVolBytes], numVolBytes); //copy volume to desire location dest, src, bytes memcpy(&inImg[i * numVolBytes], &tempVol[0], numVolBytes); //copy unsorted volume inPos[o] = i; - outPos += numVolBytes; } //for each volume free(inPos); free(volOrderIndex); @@ -2181,6 +2260,7 @@ int *nii_saveDTI(char pathoutname[], int nConvert, struct TDCMsort dcmSort[], st #endif //https://github.com/rordenlab/dcm2niix/issues/352 bool allB0 = dcmList[indx0].isDiffusion; + bool isDerived = dcmList[indx0].isDerived; if (dcmList[indx0].isDerived) allB0 = false; //e.g. FA map if ((numDti == numVol) && (numDti > 1)) @@ -2225,10 +2305,10 @@ int *nii_saveDTI(char pathoutname[], int nConvert, struct TDCMsort dcmSort[], st } if (numDti < 1) return NULL; - if ((numDti < 3) && (nConvert < 3)) + if ((numDti < 2) && (nConvert < 2)) return NULL; TDTI *vx = NULL; - if (numDti > 2) { + if (numDti > 1) { vx = (TDTI *)malloc(numDti * sizeof(TDTI)); for (int i = 0; i < numDti; i++) //for each direction for (int v = 0; v < 4; v++) //for each vector+B-value @@ -2310,11 +2390,13 @@ int *nii_saveDTI(char pathoutname[], int nConvert, struct TDCMsort dcmSort[], st for (int i = 0; i < numDti; i++) if (vx[i].V[0] > maxB0) maxB0 = vx[i].V[0]; - //for CMRR sequences unweighted volumes are not actually B=0 but they have B near zero - if (minB0 > 50) - printWarning("This diffusion series does not have a B0 (reference) volume\n"); - if ((!opts.isSortDTIbyBVal) && (minB0idx > 0)) - printMessage("Note: B0 not the first volume in the series (FSL eddy reference volume is %d)\n", minB0idx); + if (!isDerived) { //no warnings for derived data + //for CMRR sequences unweighted volumes are not actually B=0 but they have B near zero + if (minB0 > 50) + printWarning("This diffusion series does not have a B0 (reference) volume\n"); + if ((!opts.isSortDTIbyBVal) && (minB0idx > 0)) + printMessage("Note: B0 not the first volume in the series (FSL eddy reference volume is %d)\n", minB0idx); + } float kADCval = maxB0 + 1; //mark as unusual *numADC = 0; bvals = (float *)malloc(numDti * sizeof(float)); @@ -2457,6 +2539,82 @@ int *nii_saveDTI(char pathoutname[], int nConvert, struct TDCMsort dcmSort[], st vx[i].V[1] = -vx[i].V[1]; } //for each direction } //if not a mosaic + +#ifdef USING_DCM2NIIXFSWRAPPER + // make adjustments for MGH bvecs output + for (int i = 0; i < (numDti); i++) { + if (sliceDir < 0) + { + // at this point, bvecs output is calculated as not isFlipY, assuming isFlipZ, determinant is positive. + // So, bvecs first column is reversed for FSL. + // MGH conversion: not isFlipY, slice direction not flipped, determinant is negative, + // 1. we need to reverse bvecs column 1 back, + // 2. also need to reverse bvecs column 3 + + float tmp = vx[i].V[1]; + vx[i].V[1] = -vx[i].V[1]; + if (getenv("DCM2NIIXFSWRAPPER_DEBUG") != NULL && strcmp(getenv("DCM2NIIXFSWRAPPER_DEBUG"), "yes") == 0) + { + if (i < 6) + printf("nii_saveDTI() (BVECS_DEBUG) (mgh adj. sliceDir < 0) flip bvecs sign column 1: %f => %f\n", tmp, vx[i].V[1]); + } + + if (fabs(vx[i].V[3]) > FLT_EPSILON) + { + tmp = vx[i].V[3]; + vx[i].V[3] = -vx[i].V[3]; + if (getenv("DCM2NIIXFSWRAPPER_DEBUG") != NULL && strcmp(getenv("DCM2NIIXFSWRAPPER_DEBUG"), "yes") == 0) + { + if (i < 6) + printf("nii_saveDTI() (BVECS_DEBUG) (mgh adj. sliceDir < 0) flip bvecs sign column 3: %f => %f\n", tmp, vx[i].V[3]); + } + } + } + else if (abs(sliceDir) == kSliceOrientMosaicNegativeDeterminant) + { + // swap signs for every column + for (int j = 1; j < 4; j++) + { + if (fabs(vx[i].V[j]) > FLT_EPSILON) + { + float tmp = vx[i].V[j]; + vx[i].V[j] = -vx[i].V[j]; + if (getenv("DCM2NIIXFSWRAPPER_DEBUG") != NULL && strcmp(getenv("DCM2NIIXFSWRAPPER_DEBUG"), "yes") == 0) + { + if (i < 6) + printf("nii_saveDTI() (BVECS_DEBUG) (mgh adj. abs(sliceDir) == kSliceOrientMosaicNegativeDeterminant) flip bvecs sign column j: %f => %f\n", tmp, vx[i].V[j]); + } + } + } + } + else // sliceDir >= 0 && abs(sliceDir) != kSliceOrientMosaicNegativeDeterminant + { + // MGH conversion: not flip Y, image determinant is positive, bvecs first column is reversed for FSL. + // So, we need to flip bvecs first column. + if (fabs(vx[i].V[1]) > FLT_EPSILON) + { + float tmp = vx[i].V[1]; + vx[i].V[1] = -vx[i].V[1]; + if (getenv("DCM2NIIXFSWRAPPER_DEBUG") != NULL && strcmp(getenv("DCM2NIIXFSWRAPPER_DEBUG"), "yes") == 0) + { + if (i < 6) + printf("nii_saveDTI() (BVECS_DEBUG) (mgh adj. abs(sliceDir) != kSliceOrientMosaicNegativeDeterminant) flip bvecs sign column 1: %f => %f\n", tmp, vx[i].V[1]); + } + } + } + } //for each direction + + mrifsStruct.numDti = numDti; + mrifsStruct.tdti = (TDTI *)malloc(numDti * sizeof(TDTI)); + for (int i = 0; i < numDti; i++) + { + mrifsStruct.tdti[i].V[0] = vx[i].V[0]; + mrifsStruct.tdti[i].V[1] = vx[i].V[1]; + mrifsStruct.tdti[i].V[2] = vx[i].V[2]; + mrifsStruct.tdti[i].V[3] = vx[i].V[3]; + } +#endif + if (opts.isVerbose) { for (int i = 0; i < (numDti); i++) { printMessage("%d\tB=\t%g\tVec=\t%g\t%g\t%g\n", i, vx[i].V[0], vx[i].V[1], vx[i].V[2], vx[i].V[3]); @@ -2482,12 +2640,7 @@ int *nii_saveDTI(char pathoutname[], int nConvert, struct TDCMsort dcmSort[], st for (int v = 0; v < 4; v++) //for each vector+B-value dti4D->S[i].V[v] = vx[i].V[v]; } -#ifdef USING_DCM2NIIXFSWRAPPER - mrifsStruct.tdti = vx; - mrifsStruct.numDti = numDti; -#else free(vx); -#endif return volOrderIndex; } @@ -2512,12 +2665,7 @@ int *nii_saveDTI(char pathoutname[], int nConvert, struct TDCMsort dcmSort[], st fclose(fp); #endif if (isIsotropic) { //issue 405: ISOTROPIC images have bval but not bvec -#ifdef USING_DCM2NIIXFSWRAPPER - mrifsStruct.tdti = vx; - mrifsStruct.numDti = numDti; -#else free(vx); -#endif return volOrderIndex; } @@ -2547,12 +2695,7 @@ int *nii_saveDTI(char pathoutname[], int nConvert, struct TDCMsort dcmSort[], st #endif #endif -#ifdef USING_DCM2NIIXFSWRAPPER - mrifsStruct.tdti = vx; - mrifsStruct.numDti = numDti; -#else free(vx); -#endif return volOrderIndex; } // nii_saveDTI() @@ -2729,12 +2872,35 @@ int compareTFloatSort(const void *a, const void *b) { return 0; } // compareTFloatSort() + +int isSameFloatT(float a, float b, float tolerance) { + return (fabs(a - b) <= tolerance); +} + bool ensureSequentialSlicePositions(int d3, int d4, struct TDCMsort dcmSort[], struct TDICOMdata dcmList[], int verbose) { //ensure slice position is sequential: either ascending [1 2 3] or descending [3 2 1], not [1 3 2], [3 1 2] etc. //n.b. as currently designed, this will force swapDim3Dim4() for 4D data int nConvert = d3 * d4; if (d3 < 3) return true; //always consistent + //first pass: check order: issue 622 + int i = 0; + bool isSequential = true; + for (int t = 0; t < d4; t++) { //for each volume + float dx = intersliceDistanceSigned(dcmList[dcmSort[i].indx], dcmList[dcmSort[i + 1].indx]); + for (int z = 0; z < d3; z++) { //for slice + if (z > 0) { + float dx2 = intersliceDistanceSigned(dcmList[dcmSort[i - 1].indx], dcmList[dcmSort[i].indx]); + if (!isSameFloatT(dx, dx2, kSliceTolerance)) + isSequential = false; + } //if not 1st slice (which does not have prior slice) + i++; + } //for each slice + } //for each volume + if (isSequential) + return true; + //second pass: fix if required + printWarning("Instance Number (0020,0013) order is not spatial.\n"); TFloatSort *floatSort = (TFloatSort *)malloc(nConvert * sizeof(TFloatSort)); int minVol = dcmList[dcmSort[0].indx].rawDataRunNumber; int maxVol = minVol; @@ -3039,7 +3205,7 @@ int nii_createFilename(struct TDICOMdata dcm, char *niiFilename, struct TDCMopts inname[strlen(inname) - 4] = '\0'; } char outname[PATH_MAX] = {""}; - char newstr[256]; + char newstr[PATH_MAX]; if (strlen(inname) < 1) { strcpy(inname, "T%t_N%n_S%s"); } @@ -3079,7 +3245,7 @@ int nii_createFilename(struct TDICOMdata dcm, char *niiFilename, struct TDCMopts strcat(outname, dcm.seriesDescription); if (f == 'E') { isEchoReported = true; - sprintf(newstr, "%d", dcm.echoNum); + snprintf(newstr, PATH_MAX, "%d", dcm.echoNum); strcat(outname, newstr); } if (f == 'F') @@ -3109,6 +3275,12 @@ int nii_createFilename(struct TDICOMdata dcm, char *niiFilename, struct TDCMopts strcat(outname, "Ph"); else if (dcm.manufacturer == kMANUFACTURER_SIEMENS) strcat(outname, "Si"); + else if (dcm.manufacturer == kMANUFACTURER_MEDISO) + strcat(outname, "Me"); + else if (dcm.manufacturer == kMANUFACTURER_MRSOLUTIONS) + strcat(outname, "MR"); + else if (dcm.manufacturer == kMANUFACTURER_HYPERFINE) + strcat(outname, "Hy"); else strcat(outname, "NA"); //manufacturer name not available } @@ -3125,28 +3297,28 @@ int nii_createFilename(struct TDICOMdata dcm, char *niiFilename, struct TDCMopts printWarning("Unable to append protocol name (0018,1030) to filename (it is empty).\n"); } if (f == 'R') { - sprintf(newstr, "%d", dcm.imageNum); + snprintf(newstr, PATH_MAX, "%d", dcm.imageNum); strcat(outname, newstr); isImageNumReported = true; } if (f == 'Q') strcat(outname, dcm.scanningSequence); if (f == 'S') { - sprintf(newstr, "%ld", dcm.seriesNum); + snprintf(newstr, PATH_MAX, "%ld", dcm.seriesNum); strcat(outname, newstr); isSeriesReported = true; } if (f == 'T') { - sprintf(newstr, "%0.0f", dcm.dateTime); + snprintf(newstr, PATH_MAX, "%0.0f", dcm.dateTime); strcat(outname, newstr); } if (f == 'U') { if (opts.isRenameNotConvert) { - sprintf(newstr, "%d", dcm.acquNum); + snprintf(newstr, PATH_MAX, "%d", dcm.acquNum); strcat(outname, newstr); //isAcquisitionReported = true; } else { - sprintf(newstr, "%d", dcm.acquNum); + snprintf(newstr, PATH_MAX, "%d", dcm.acquNum); strcat(outname, newstr); #ifdef mySegmentByAcq //isAcquisitionReported = true; @@ -3176,31 +3348,31 @@ int nii_createFilename(struct TDICOMdata dcm, char *niiFilename, struct TDCMopts if (f == 'X') strcat(outname, dcm.studyID); if ((f == 'Y') && (dcm.rawDataRunNumber >= 0)) { - sprintf(newstr, "%d", dcm.rawDataRunNumber); //GE (0019,10A2) else (0020,0100) + snprintf(newstr, PATH_MAX, "%d", dcm.rawDataRunNumber); //GE (0019,10A2) else (0020,0100) strcat(outname, newstr); } if (f == 'Z') strcat(outname, dcm.sequenceName); if ((f >= '0') && (f <= '9')) { if ((pos < strlen(inname)) && (toupper(inname[pos + 1]) == 'S')) { - char zeroPad[12] = {""}; - sprintf(zeroPad, "%%0%dd", f - '0'); - sprintf(newstr, zeroPad, dcm.seriesNum); + char zeroPad[128] = {""}; + snprintf(zeroPad, 128, "%%0%dd", f - '0'); + snprintf(newstr, PATH_MAX, zeroPad, dcm.seriesNum); strcat(outname, newstr); pos++; // e.g. %3f requires extra increment: skip both number and following character } if ((pos < strlen(inname)) && (toupper(inname[pos + 1]) == 'R')) { - char zeroPad[12] = {""}; - sprintf(zeroPad, "%%0%dd", f - '0'); - sprintf(newstr, zeroPad, dcm.imageNum); + char zeroPad[128] = {""}; + snprintf(zeroPad, 128, "%%0%dd", f - '0'); + snprintf(newstr, PATH_MAX, zeroPad, dcm.imageNum); isImageNumReported = true; strcat(outname, newstr); pos++; // e.g. %3f requires extra increment: skip both number and following character } if ((pos < strlen(inname)) && (toupper(inname[pos + 1]) == 'Y') && (dcm.rawDataRunNumber >= 0)) { - char zeroPad[12] = {""}; - sprintf(zeroPad, "%%0%dd", f - '0'); - sprintf(newstr, zeroPad, dcm.rawDataRunNumber); //GE (0019,10A2) else (0020,0100) + char zeroPad[128] = {""}; + snprintf(zeroPad, 128, "%%0%dd", f - '0'); + snprintf(newstr, PATH_MAX, zeroPad, dcm.rawDataRunNumber); //GE (0019,10A2) else (0020,0100) strcat(outname, newstr); pos++; // e.g. %3f requires extra increment: skip both number and following character } @@ -3215,7 +3387,7 @@ int nii_createFilename(struct TDICOMdata dcm, char *niiFilename, struct TDCMopts strcat(outname, newstr); } if ((isAddNamePostFixes) && (!isCoilReported) && (dcm.isCoilVaries)) { - //sprintf(newstr, "_c%d", dcm.coilNum); + //snprintf(newstr, PATH_MAX, "_c%d", dcm.coilNum); //strcat (outname,newstr); strcat(outname, "_c"); strcat(outname, dcm.coilName); @@ -3227,23 +3399,23 @@ int nii_createFilename(struct TDICOMdata dcm, char *niiFilename, struct TDCMopts if ((isAddNamePostFixes) && (!isEchoReported) && ((dcm.isMultiEcho) || (dcm.echoNum > 1))) { //multiple echoes saved as same series #endif if ((dcm.echoNum < 1) && (dcm.TE > 0)) - sprintf(newstr, "_e%g", dcm.TE); //issue568: Siemens XA20 might omit echo number + snprintf(newstr, PATH_MAX, "_e%g", dcm.TE); //issue568: Siemens XA20 might omit echo number else - sprintf(newstr, "_e%d", dcm.echoNum); + snprintf(newstr, PATH_MAX, "_e%d", dcm.echoNum); strcat(outname, newstr); isEchoReported = true; } if ((isAddNamePostFixes) && (!isSeriesReported) && (!isEchoReported) && (dcm.echoNum > 1)) { //last resort: user provided no method to disambiguate echo number in filename - sprintf(newstr, "_e%d", dcm.echoNum); + snprintf(newstr, PATH_MAX, "_e%d", dcm.echoNum); strcat(outname, newstr); isEchoReported = true; } if ((dcm.isNonParallelSlices) && (!isImageNumReported)) { - sprintf(newstr, "_i%05d", dcm.imageNum); + snprintf(newstr, PATH_MAX, "_i%05d", dcm.imageNum); strcat(outname, newstr); } /*if (dcm.maxGradDynVol > 0) { //Philips segmented - sprintf(newstr, "_v%04d", dcm.gradDynVol+1); //+1 as indexed from zero + snprintf(newstr, PATH_MAX, "_v%04d", dcm.gradDynVol+1); //+1 as indexed from zero strcat (outname,newstr); }*/ if ((isAddNamePostFixes) && (dcm.isHasImaginary)) { @@ -3261,7 +3433,7 @@ int nii_createFilename(struct TDICOMdata dcm, char *niiFilename, struct TDCMopts strcat(outname, "Mag"); //Philips enhanced with BOTH phase and Magnitude in single file } 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)); + snprintf(newstr, PATH_MAX, "_t%d", (int)roundf(dcm.triggerDelayTime)); strcat(outname, newstr); } //could add (isAddNamePostFixes) to these next two, but consequences could be catastrophic @@ -3339,8 +3511,8 @@ int nii_createFilename(struct TDICOMdata dcm, char *niiFilename, struct TDCMopts mkdir(newdir, 0700); #endif } - char ch[12] = {""}; - sprintf(ch, "%c", outname[pos]); + char ch[128] = {""}; + snprintf(ch, 128, "%c", outname[pos]); strcat(newdir, ch); } } @@ -3605,6 +3777,12 @@ void nii_saveAttributes(struct TDICOMdata &data, struct nifti_1_header &header, case kMANUFACTURER_CANON: images->addAttribute("manufacturer", "Canon"); break; + case kMANUFACTURER_MRSOLUTIONS: + images->addAttribute("manufacturer", "MRSolutions"); + break; + case kMANUFACTURER_HYPERFINE: + images->addAttribute("manufacturer", "Hyperfine"); + break; } images->addAttribute("scannerModelName", data.manufacturersModelName); images->addAttribute("imageType", data.imageType); @@ -3714,11 +3892,11 @@ int pigz_File(char *fname, struct TDCMopts opts, size_t imgsz) { strcat(command, opts.pigzname); if ((opts.gzLevel > 0) && (opts.gzLevel < 12)) { char newstr[256]; - sprintf(newstr, "\"%s -n -f -%d \"", blockSize, opts.gzLevel); + snprintf(newstr, 256, "\"%s -n -f -%d \"", blockSize, opts.gzLevel); strcat(command, newstr); } else { char newstr[256]; - sprintf(newstr, "\"%s -n \"", blockSize); + snprintf(newstr, 256, "\"%s -n \"", blockSize); strcat(command, newstr); } strcat(command, fname); @@ -4231,7 +4409,7 @@ int zmat_run(const size_t inputsize, unsigned char *inputstr, size_t *outputsize /** perform compression or encoding */ if(zipid==zmBase64){ /** base64 encoding */ - *outputbuf=base64_encode((const unsigned char*)inputstr, inputsize, outputsize); + *outputbuf=base64_encode((const unsigned char*)inputstr, inputsize, outputsize); }else if(zipid==zmZlib || zipid==zmGzip){ /** zlib (.zip) or gzip (.gz) compression */ if(zipid==zmZlib){ @@ -4551,7 +4729,7 @@ int nii_savejnii(char *niiFilename, struct nifti_1_header hdr, unsigned char *im size_t compressedbytes, totalbytes; unsigned char *compressed=NULL, *buf=NULL; - /*jnifti convers code-based header fields to human-readable/standardized strings*/ + /*jnifti converts code-based header fields to human-readable/standardized strings*/ int datatypeidx; const char *datatypestr[]={"uint8","int16","int32","single","complex64","double", "rgb24" ,"int8","uint16","uint32","int64","uint64", @@ -4909,7 +5087,7 @@ int nii_saveNII(char *niiFilename, struct nifti_1_header hdr, unsigned char *im, strcat(command, opts.pigzname); if ((opts.gzLevel > 0) && (opts.gzLevel < 12)) { char newstr[256]; - sprintf(newstr, "\" -n -f -%d > \"", opts.gzLevel); + snprintf(newstr, 256, "\" -n -f -%d > \"", opts.gzLevel); strcat(command, newstr); } else strcat(command, "\" -n -f > \""); //current versions of pigz (2.3) built on Windows can hang if the filename is included, presumably because it is not finding the path characters ':\' @@ -4986,9 +5164,9 @@ int nii_saveNII3D(char *niiFilename, struct nifti_1_header hdr, unsigned char *i char zeroPad[PATH_MAX] = {""}; double fnVol = nVol; int zeroPadLen = (1 + log10(fnVol)); - sprintf(zeroPad, "%%s_%%0%dd", zeroPadLen); + snprintf(zeroPad, PATH_MAX, "%%s_%%0%dd", zeroPadLen); for (int i = 1; i <= nVol; i++) { - sprintf(fname, zeroPad, niiFilename, i); + snprintf(fname, 2048, zeroPad, niiFilename, i); if (nii_saveNII(fname, hdr1, (unsigned char *)&im[pos], opts, d) == EXIT_FAILURE) return EXIT_FAILURE; pos += imgsz; @@ -4999,12 +5177,16 @@ int nii_saveNII3D(char *niiFilename, struct nifti_1_header hdr, unsigned char *i void nii_storeIntegerScaleFactor(int scale, struct nifti_1_header *hdr) { //appends NIfTI header description field with " isN" where N is integer scaling char newstr[256]; - sprintf(newstr, " is%d", scale); + snprintf(newstr, 256, " is%d", scale); if ((strlen(newstr) + strlen(hdr->descrip)) < 80) strcat(hdr->descrip, newstr); } -void nii_mask12bit(unsigned char *img, struct nifti_1_header *hdr) { +int int12toint16(int U12) { + return (short)(U12 & 0xFFF) - ((U12 & 0x800) << 1); +} + +void nii_mask12bit(unsigned char *img, struct nifti_1_header *hdr, bool isSigned) { //https://github.com/rordenlab/dcm2niix/issues/251 if (hdr->datatype != DT_INT16) return; @@ -5016,8 +5198,15 @@ void nii_mask12bit(unsigned char *img, struct nifti_1_header *hdr) { if (nVox < 1) return; int16_t *img16 = (int16_t *)img; - for (int i = 0; i < nVox; i++) - img16[i] = img16[i] & 4095; //12 bit data ranges from 0..4095, any other values are overflow + //issue 688 + if (isSigned) { + for (int i = 0; i < nVox; i++) + img16[i] = int12toint16(img16[i]); //signed 12 bit data ranges from 0..4095, any other values are overflow + + } else { + for (int i = 0; i < nVox; i++) + 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) { @@ -5175,10 +5364,6 @@ int siemensCtKludge(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d return nConvert; //all images in sequential order } // siemensCtKludge() -int isSameFloatT(float a, float b, float tolerance) { - return (fabs(a - b) <= tolerance); -} - void adjustOriginForNegativeTilt(struct nifti_1_header *hdr, float shiftPxY) { if (hdr->sform_code > 0) { // Adjust the srow_* offsets using srow_y @@ -5754,7 +5939,7 @@ void checkSliceTiming(struct TDICOMdata *d, struct TDICOMdata *d1, int verbose, int nSlices = 0; while ((nSlices < kMaxEPI3D) && (d->CSA.sliceTiming[nSlices] >= 0.0)) nSlices++; - if (nSlices < 1) + if (nSlices < 2) return; if (d->CSA.sliceTiming[kMaxEPI3D - 1] < -1.0) //the value -2.0 is used as a flag for negative MosaicRefAcqTimes in checkSliceTimes(), see issue 271 printWarning("Adjusting for negative MosaicRefAcqTimes (issue 271).\n"); @@ -5838,6 +6023,10 @@ void checkSliceTiming(struct TDICOMdata *d, struct TDICOMdata *d1, int verbose, if ((minT1 < 0.0) && (d->rtia_timerGE >= 0.0)) return; //use rtia timer if (minT1 < 0.0) { //https://github.com/neurolabusc/MRIcroGL/issues/31 + if (d->isDerived) { //slice timing not relevant for derived data, values mangled with Siemens XA30 + d->CSA.sliceTiming[0] = -1.0; + return; + } if (d->modality == kMODALITY_MR) printWarning("Siemens MoCo? Bogus slice timing (range %g..%g, TR=%g seconds)\n", minT1, maxT1, TRms); return; @@ -5856,36 +6045,40 @@ void checkSliceTiming(struct TDICOMdata *d, struct TDICOMdata *d1, int verbose, printMessage("CSA slice timing based on 2nd volume, 1st volume corrupted (CMRR bug, range %g..%g, TR=%g ms)\n", minT, maxT, TRms); } //checkSliceTiming() +void setMultiBandFactor(int dim3, uint64_t indx0, struct TDICOMdata *dcmList) { + float mn = dcmList[indx0].CSA.sliceTiming[0]; + //first pass: find minimum + for (int v = 0; v < dim3; v++) + mn = fminf(dcmList[indx0].CSA.sliceTiming[v],mn); + //second pass: all times relative to min (i.e. make min = 0) + int mb = 0; + for (int v = 0; v < dim3; v++) { + dcmList[indx0].CSA.sliceTiming[v] -= mn; + if (isSameFloatGE(dcmList[indx0].CSA.sliceTiming[v], 0.0)) + mb++; + } + if ((dcmList[indx0].CSA.multiBandFactor < 2) && (mb > 1) && (mb < dim3)) + dcmList[indx0].CSA.multiBandFactor = mb; +} // setMultiBandFactor() + void sliceTimingXA(struct TDCMsort *dcmSort, struct TDICOMdata *dcmList, struct nifti_1_header *hdr, int verbose, const char *filename, int nConvert) { //Siemens XA10 slice timing // Ignore first volume: For an example of erroneous first volume timing, see series 10 (Functional_w_SMS=3) https://github.com/rordenlab/dcm2niix/issues/240 // an alternative would be to use 0018,9074 - this would need to be converted from DT to Secs, and is scrambled if de-identifies data see enhanced de-identified series 26 from issue 236 uint64_t indx0 = dcmSort[0].indx; //first volume - if ((!dcmList[indx0].isXA10A) || (hdr->dim[3] < 1)) + if ((!dcmList[indx0].isXA10A) || (hdr->dim[3] < 1) || (hdr->dim[4] < 1)) return; - if ((nConvert == (hdr->dim[3] * hdr->dim[4])) && (hdr->dim[3] < (kMaxEPI3D - 1)) && (hdr->dim[3] > 1) && (hdr->dim[4] > 1)) { - //XA11 2D classic + if ((nConvert == (hdr->dim[3] * hdr->dim[4])) && (hdr->dim[3] < (kMaxEPI3D - 1)) && (hdr->dim[3] > 1)) { + //XA11 2D classic: nb XA30 in `MFSPLIT` will save each 3D volume from 4D timeseries as a unique series number! for (int v = 0; v < hdr->dim[3]; v++) dcmList[indx0].CSA.sliceTiming[v] = dcmList[dcmSort[v].indx].CSA.sliceTiming[0]; + setMultiBandFactor(hdr->dim[3], indx0, dcmList); } else if ((nConvert == (hdr->dim[4])) && (hdr->dim[3] < (kMaxEPI3D - 1)) && (hdr->dim[3] > 1) && (hdr->dim[4] > 1)) { //XA10 mosaics - these are missing a lot of information - float mn = dcmList[dcmSort[1].indx].CSA.sliceTiming[0]; //get slice timing from second volume - for (int v = 0; v < hdr->dim[3]; v++) { + for (int v = 0; v < hdr->dim[3]; v++) dcmList[indx0].CSA.sliceTiming[v] = dcmList[dcmSort[1].indx].CSA.sliceTiming[v]; - if (dcmList[indx0].CSA.sliceTiming[v] < mn) - mn = dcmList[indx0].CSA.sliceTiming[v]; - } - if (mn < 0.0) - mn = 0.0; - int mb = 0; - for (int v = 0; v < hdr->dim[3]; v++) { - dcmList[indx0].CSA.sliceTiming[v] -= mn; - if (isSameFloatGE(dcmList[indx0].CSA.sliceTiming[v], 0.0)) - mb++; - } - if ((dcmList[indx0].CSA.multiBandFactor < 2) && (mb > 1)) - dcmList[indx0].CSA.multiBandFactor = mb; + setMultiBandFactor(hdr->dim[3], indx0, dcmList); return; //we have subtracted min } //issue429: subtract min @@ -5960,6 +6153,7 @@ void readSoftwareVersionsGE(char softwareVersionsGE[], int verbose, char geVersi // softwareVersionsGE // "27\LX\MR Software release:RX27.0_R02_1831.a" -> 27 // "28\LX\MR29.1_EA_2039.g" -> 29 + // "30\LX\SIGNA_LX1.MR30.0_R01_2236.d" -> 30; see issue 634 // geVersionPrefix // RX27.0_R02_1831.a -> RX // MR29.1_EA_2039.g -> MR @@ -5977,16 +6171,36 @@ void readSoftwareVersionsGE(char softwareVersionsGE[], int verbose, char geVersi // RX27.0_R02_1831.a -> 2 // MR29.1_EA_2039.g -> 0 int len = 0; + bool ismatched = false; + int substrlen = 0; + + // If softwareVersionsGE is 30\LX\SIGNA_LX1.MR30.0_R01_2236.d; see issue 634 + char *sepStart = strstr(softwareVersionsGE, "SIGNA_LX1"); + if (ismatched == false) { + if (sepStart != NULL) { + ismatched = true; + substrlen = strlen("SIGNA_LX1"); + sepStart += substrlen+1; + } + } // If softwareVersionsGE is 27\LX\MR Software release:RX27.0_R02_1831.a - char *sepStart = strchr(softwareVersionsGE, ':'); - if (sepStart == NULL) { - // If softwareVersionsGE is 28\LX\MR29.1_EA_2039.g + if (ismatched == false) { + sepStart = strstr(softwareVersionsGE, "MR Software release"); + if (sepStart != NULL) { + ismatched = true; + substrlen = strlen("MR Software release"); + sepStart += substrlen+1; + } + } + // If softwareVersionsGE is 28\LX\MR29.1_EA_2039.g + if (ismatched == false) { sepStart = strrchr(softwareVersionsGE, '\\'); - if (sepStart == NULL) - return; + if (sepStart != NULL) { + ismatched = true; + sepStart += 1; + } } - sepStart += 1; - len = 11; + len = 11; // RX27.0_R02_ char *versionString = (char *)malloc(sizeof(char) * len); versionString[len - 1] = 0; memcpy(versionString, sepStart, len); @@ -6002,6 +6216,7 @@ void readSoftwareVersionsGE(char softwareVersionsGE[], int verbose, char geVersi *geMajorVersion = (float)*geMajorVersionInt + (float)0.1 * (float)*geMinorVersionInt; *is27r3 = ((*geMajorVersion >= 27.1) || ((*geMajorVersionInt == 27) && (*geReleaseVersionInt >= 3))); if (verbose > 1) { + printMessage("GE Software VersionSting: %s\n", softwareVersionsGE); printMessage("GE Software VersionPrefix: %s\n", geVersionPrefix); printMessage("GE Software MajorVersion: %d\n", *geMajorVersionInt); printMessage("GE Software MinorVersion: %d\n", *geMinorVersionInt); @@ -6070,7 +6285,7 @@ void sliceTimingGE(struct TDICOMdata *d, const char *filename, struct TDCMopts o //start version check: float geMajorVersion = 0; int geMajorVersionInt = 0, geMinorVersionInt = 0, geReleaseVersionInt = 0; - char geVersionPrefix[2] = " "; + char geVersionPrefix[3] = ""; bool is27r3 = false; readSoftwareVersionsGE(d->softwareVersions, opts.isVerbose, geVersionPrefix, &geMajorVersion, &geMajorVersionInt, &geMinorVersionInt, &geReleaseVersionInt, &is27r3); //readSoftwareVersionsGE(&geMajorVersion); @@ -6110,7 +6325,7 @@ void sliceTimingGE(struct TDICOMdata *d, const char *filename, struct TDCMopts o if (nSlices != hdr->dim[3]) //redundant with locationsInAcquisition check? printWarning("Missing DICOMs, number of slices estimated (%d) differs from Protocol Block (0025,101B) report (%d).\n", hdr->dim[3], nSlices); d->CSA.multiBandFactor = max(d->CSA.multiBandFactor, mbAccel); - bool isInterleaved = (sliceOrderGE != 0); + bool isInterleaved = true; groupDelay *= 1000.0; //sec -> ms // // Estimate GE Slice Time only for EPI Multi-Phase (epi) or EPI fMRI/BrainWave (epiRT) @@ -6119,6 +6334,7 @@ void sliceTimingGE(struct TDICOMdata *d, const char *filename, struct TDCMopts o 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 + isInterleaved = (sliceOrderGE != 0); d->epiVersionGE = 1; d->internalepiVersionGE = 1; // 'EPI'(gradient echo)/'EPI2'(spin echo) if (!isSameFloatGE(groupDelay, d->groupDelay)) @@ -6139,11 +6355,34 @@ void sliceTimingGE(struct TDICOMdata *d, const char *filename, struct TDCMopts o return; } } - // Diffusion (Unsupported) + // Diffusion (see issue 635) else if ((d->epiVersionGE == 2) || (d->internalepiVersionGE == 2) || (strstr(ioptGE, "DIFF") != NULL)) { - printWarning("Unable to compute slice times for GE Diffusion\n"); - d->CSA.sliceTiming[0] = -1.0; - return; + // diffusion gradient cycling OFF + if (opts.diffCyclingModeGE >= 0) + d->diffCyclingModeGE = opts.diffCyclingModeGE; + if ((d->diffCyclingModeGE == kGE_DIFF_CYCLING_SPOFF) || (d->diffCyclingModeGE == kGE_DIFF_CYCLING_OFF)) + is27r3 = false; + else if (d->diffCyclingModeGE == kGE_DIFF_CYCLING_ALLTR) { + printWarning("Unable to compute slice times for GE Diffusion:Cycling\n"); + d->CSA.sliceTiming[0] = -1.0; + return; + } + // TO DO: support 2TR/3TR cycling mode + else if (d->diffCyclingModeGE == kGE_DIFF_CYCLING_2TR) { + printWarning("Unable to compute slice times for GE Diffusion:2TR-Cycling\n"); + d->CSA.sliceTiming[0] = -1.0; + return; + } + else if (d->diffCyclingModeGE == kGE_DIFF_CYCLING_3TR) { + printWarning("Unable to compute slice times for GE Diffusion:3TR-Cyclin\n"); + d->CSA.sliceTiming[0] = -1.0; + return; + } + else { + printWarning("Unable to compute slice times for GE Diffusion\n"); + d->CSA.sliceTiming[0] = -1.0; + return; + } } // Others (Unsupported) else { @@ -6473,6 +6712,10 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d dti4D->frameDuration[0] = -1; dti4D->frameReferenceTime[0] = -1; } + if (strlen(dcmList[indx0].patientOrient) < 3) + printWarning("PatientOrient (0018,5100) not specified (issue 642).\n"); + if (dcmList[indx0].isQuadruped) + printWarning("Anatomical Orientation Type (0010,2210) is QUADRUPED: rotate coordinates accordingly (issue 642)\n"); #ifdef newTilt //see issue 254 if (((nConvert > 1) || (dcmList[indx0].xyzDim[3] > 1)) && ((dcmList[indx0].modality == kMODALITY_CT) || (dcmList[indx0].isXRay) || (dcmList[indx0].gantryTilt > 0.0))) { //issue372: enhanced DICOMs can also have gantry tilt dcmList[indx0].gantryTilt = computeGantryTiltPrecise(dcmList[indx0], dcmList[indxEnd], opts.isVerbose); @@ -6520,7 +6763,7 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d mrifsStruct.tdicomData = dcmList[indx]; // first in sorted list dcmSort #endif - struct nifti_1_header hdr0; + struct nifti_1_header hdr0 = {0}; unsigned char *img = nii_loadImgXL(nameList->str[indx], &hdr0, dcmList[indx], iVaries, opts.compressFlag, opts.isVerbose, dti4D); if (strlen(opts.imageComments) > 0) { for (int i = 0; i < 24; i++) @@ -6542,6 +6785,7 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d //printMessage(" %d %d %d %d %lu\n", hdr0.dim[1], hdr0.dim[2], hdr0.dim[3], hdr0.dim[4], (unsigned long)[imgM length]); bool isHasOverlay = dcmList[indx0].isHasOverlay; + bool isDerived = dcmList[indx0].isDerived; 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; @@ -6595,7 +6839,7 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d } } } - if (nAcq != nSamePos) + if ((nAcq != nSamePos) && (!isDerived)) //Siemens Derived FA-RGB images have bogus spatial data printWarning("Expected %d volumes but found spatial position repeats %d times.\n", nAcq, nSamePos); //end validate number of spatial volumes if ((nAcq > 1) && ((nConvert / nAcq) > 1) && ((nConvert % nAcq) == 0)) { @@ -6746,17 +6990,15 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d } //if PET //next: detect variable inter-slice distance float dx = intersliceDistance(dcmList[dcmSort[0].indx], dcmList[dcmSort[1].indx]); -#ifdef myInstanceNumberOrderIsNotSpatial +//#ifdef myInstanceNumberOrderIsNotSpatial if (!isSameFloat(dx, 0.0)) //only for XYZT, not TXYZ: perhaps run for swapDim3Dim4? Extremely rare anomaly if (!ensureSequentialSlicePositions(hdr0.dim[3], hdr0.dim[4], dcmSort, dcmList, opts.isVerbose)) dx = intersliceDistance(dcmList[dcmSort[0].indx], dcmList[dcmSort[1].indx]); indx0 = dcmSort[0].indx; - //if (nConvert > 1) - // indx1 = dcmSort[1].indx; -#endif +//#endif bool dxVaries = false; for (int i = 1; i < nConvert; i++) - if (!isSameFloatT(dx, intersliceDistance(dcmList[dcmSort[i - 1].indx], dcmList[dcmSort[i].indx]), 0.2)) + if (!isSameFloatT(dx, intersliceDistance(dcmList[dcmSort[i - 1].indx], dcmList[dcmSort[i].indx]), kSliceTolerance)) dxVaries = true; if (hdr0.dim[4] < 2) { if (dxVaries) { @@ -6804,7 +7046,7 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d dxVaries = false; dx = intersliceDistance(dcmList[dcmSort[0].indx], dcmList[dcmSort[1].indx]); for (int i = 1; i < nConvert; i++) - if (!isSameFloatT(dx, intersliceDistance(dcmList[dcmSort[i - 1].indx], dcmList[dcmSort[i].indx]), 0.2)) + if (!isSameFloatT(dx, intersliceDistance(dcmList[dcmSort[i - 1].indx], dcmList[dcmSort[i].indx]), kSliceTolerance)) dxVaries = true; for (int i = 1; i < nConvert; i++) sliceMMarray[i] = intersliceDistance(dcmList[dcmSort[0].indx], dcmList[dcmSort[i].indx]); @@ -7035,7 +7277,7 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d int *volOrderIndex = nii_saveDTI(pathoutname, nConvert, dcmSort, dcmList, opts, sliceDir, dti4D, &numADC, hdr0.dim[4]); 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); + nii_mask12bit(imgM, &hdr0, dcmList[dcmSort[0].indx].isSigned); if ((opts.saveFormat == kSaveFormatMGH) && (hdr0.datatype == DT_UINT16)) imgM = nii_uint16toFloat32(imgM, &hdr0, opts.isVerbose); if ((opts.isMaximize16BitRange == kMaximize16BitRange_True) && (hdr0.datatype == DT_INT16)) { @@ -7049,7 +7291,7 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d #ifndef USING_DCM2NIIXFSWRAPPER printMessage("Convert %d DICOM as %s (%dx%dx%dx%d)\n", nConvert, pathoutname, hdr0.dim[1], hdr0.dim[2], hdr0.dim[3], hdr0.dim[4]); #else - printMessage( "Convert %d DICOM (%dx%dx%dx%d)\n", nConvert, hdr0.dim[1],hdr0.dim[2],hdr0.dim[3],hdr0.dim[4]); + printMessage( "Convert %d DICOM (%dx%dx%dx%d)\n", nConvert, hdr0.dim[1],hdr0.dim[2],hdr0.dim[3],hdr0.dim[4]); #endif #ifndef USING_R fflush(stdout); //show immediately if run from MRIcroGL GUI @@ -7101,6 +7343,20 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d float c = cos(thetaRad); if (!isSameFloatGE(c, 0.0)) { mat33 shearMat; + //gantry tilt formula changed with issue697 + /*printf("a=[%g %g %g %g; %g %g %g %g; %g %g %g %g; 0 0 0 1];\n", + hdr0.srow_x[0], hdr0.srow_x[1], hdr0.srow_x[2], hdr0.srow_x[3], + hdr0.srow_y[0], hdr0.srow_y[1], hdr0.srow_y[2], hdr0.srow_y[3], + hdr0.srow_z[0], hdr0.srow_z[1], hdr0.srow_z[2], hdr0.srow_z[3] + );*/ + hdr0.srow_y[2] = 0.0; //remove gantry tilt + hdr0.srow_z[2] = hdr0.pixdim[3]; //retain distance between slices + /*printf("b=[%g %g %g %g; %g %g %g %g; %g %g %g %g; 0 0 0 1];\n", + hdr0.srow_x[0], hdr0.srow_x[1], hdr0.srow_x[2], hdr0.srow_x[3], + hdr0.srow_y[0], hdr0.srow_y[1], hdr0.srow_y[2], hdr0.srow_y[3], + hdr0.srow_z[0], hdr0.srow_z[1], hdr0.srow_z[2], hdr0.srow_z[3] + );*/ + /* LOAD_MAT33(shearMat, 1.0, 0.0, 0.0, 0.0, 1.0, sin(thetaRad) / c, 0.0, 0.0, 1.0); @@ -7114,6 +7370,7 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d s.m[1][0], s.m[1][1], s.m[1][2], hdr0.srow_y[3], s.m[2][0], s.m[2][1], s.m[2][2], hdr0.srow_z[3]); setQSForm(&hdr0, shearForm, true); + */ } //avoid div/0: cosine not zero } //if gantry tilt //end: gantry tilt we need to save the shear in the transform @@ -7153,7 +7410,7 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d char pathoutnameROI[2048] = {""}; strcat(pathoutnameROI, pathoutname); char append[128] = {""}; - sprintf(append, "_ROI%d", j + 1); + snprintf(append, 127, "_ROI%d", j + 1); strcat(pathoutnameROI, append); struct nifti_1_header hdrr = hdrrx; hdrr.dim[0] = 3; @@ -7252,6 +7509,27 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d } // saveDcm2NiiCore() int saveDcm2Nii(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata dcmList[], struct TSearchList *nameList, struct TDCMopts opts, struct TDTI4D *dti4D) { +#ifdef USING_DCM2NIIXFSWRAPPER + if (opts.isDumpNotConvert) { + int indx0 = dcmSort[0].indx; + if (opts.isIgnoreSeriesInstanceUID) + printMessage("%d %s %s (total %d)\n", dcmList[indx0].seriesUidCrc, dcmList[indx0].protocolName, nameList->str[indx0], nConvert); + else + printMessage("%d %ld %s %s (total %d)\n", dcmList[indx0].seriesUidCrc, dcmList[indx0].seriesNum, dcmList[indx0].protocolName, nameList->str[indx0], nConvert); + +#if 1 + for (int i = 0; i < nConvert; i++) { + int indx = dcmSort[i].indx; + if (opts.isIgnoreSeriesInstanceUID) + printMessage("\t#\%d: %d %s\n", i+1, dcmList[indx].seriesUidCrc, nameList->str[indx]); + else + printMessage("\t#\%d: %d %ld %s\n", i+1, dcmList[indx].seriesUidCrc, dcmList[indx].seriesNum, nameList->str[indx]); + } +#endif + + return 0; + } +#endif //this wrapper does nothing if all the images share the same echo time and scale // however, it segments images when these properties vary uint64_t indx = dcmSort[0].indx; @@ -7515,7 +7793,7 @@ bool isSameSet(struct TDICOMdata d1, struct TDICOMdata d2, struct TDCMopts *opts } if ((d1.manufacturer == kMANUFACTURER_SIEMENS) && (strcmp(d1.protocolName, d2.protocolName) == 0) && (strlen(d1.softwareVersions) > 4) && (strlen(d1.sequenceName) > 4) && (strlen(d2.sequenceName) > 4)) { if (strstr(d1.sequenceName, "_ep_b") && strstr(d2.sequenceName, "_ep_b") && (strstr(d1.softwareVersions, "VB13") || strstr(d1.softwareVersions, "VB12"))) { - //Siemens B12/B13 users with a "DWI" but not "DTI" license would ofter create multi-series acquisitions + //Siemens B12/B13 users with a "DWI" but not "DTI" license would often create multi-series acquisitions if (!warnings->forceStackSeries) printMessage("Diffusion images stacked despite varying series number (early Siemens DTI).\n"); warnings->forceStackSeries = true; @@ -7576,7 +7854,18 @@ bool isSameSet(struct TDICOMdata d1, struct TDICOMdata d2, struct TDCMopts *opts // *isMultiEcho = true; //} #ifdef USING_DCM2NIIXFSWRAPPER - printf("isForceStackSameSeries = true, seriesNum %ld, %ld, seriesInstanceUidCrc %d, %d\n", d1.seriesNum, d2.seriesNum, d1.seriesUidCrc, d2.seriesUidCrc); + /* for mgh conversion set opts->isForceStackSameSeries = 1 by default, *isMultiEcho, *isNonParallelSlices, and *isCoilVaries remain unchanged. + * + * local variable isForceStackSeries is set to true if following condition met: + * if ((opts->isForceStackDCE) && (d1.isStackableSeries) && (d2.isStackableSeries) && (d1.seriesNum != d2.seriesNum)) { + * if (!warnings->forceStackSeries) + * printMessage("Volumes stacked despite varying series number (use '-m o' to turn off merging).\n"); + * warnings->forceStackSeries = true; + * isForceStackSeries = true; + * } + */ + + //printf("isForceStackSameSeries = true, seriesNum %ld, %ld, seriesInstanceUidCrc %d, %d\n", d1.seriesNum, d2.seriesNum, d1.seriesUidCrc, d2.seriesUidCrc); #endif return true; //we will stack these images, even if they differ in the following attributes } @@ -7586,6 +7875,20 @@ bool isSameSet(struct TDICOMdata d1, struct TDICOMdata d2, struct TDCMopts *opts warnings->phaseVaries = true; return false; } + if (!(isSameFloat(d1.TR, d2.TR))) { + if (!warnings->echoVaries) + printMessage("Slices not stacked: TR varies (%g, %g, issue 641). Use 'merge 2D slices' option to force stacking\n", d1.TR, d2.TR); + *isMultiEcho = true; + warnings->echoVaries = true; + return false; + } + if (!(isSameFloat(d1.flipAngle, d2.flipAngle))) { + if (!warnings->echoVaries) + printMessage("Slices not stacked: flip angle varies (%g, %g, issue 646).\n", d1.TR, d2.TR); + *isMultiEcho = true; + warnings->echoVaries = true; + return false; + } //if ((d1.TE != d2.TE) || (d1.echoNum != d2.echoNum)) { if ((!(isSameFloat(d1.TE, d2.TE))) || (d1.echoNum != d2.echoNum)) { if ((!warnings->echoVaries) && (d1.isXRay)) //for CT/XRay we check DICOM tag 0018,1152 (XRayExposure) @@ -8126,14 +8429,19 @@ int nii_loadDirCore(char *indir, struct TDCMopts *opts) { } size_t nDcm = nameList.numItems; printMessage("Found %lu DICOM file(s)\n", nameList.numItems); //includes images and other non-image DICOMs + if (opts->onlySearchDirForDICOM == 2) { + printMessage("List of DICOM file(s):\n"); + for (int i = 0; i < nameList.numItems; i++) + printMessage("%s\n", nameList.str[i]); + printMessage("End of list (%lu in total)\n", nameList.numItems); + } #ifdef myTimer if (opts->isProgress > 1) printMessage("Stage 1 (Count number of DICOMs) required %f seconds.\n", ((float)(clock() - start)) / CLOCKS_PER_SEC); start = clock(); #endif if (opts->isProgress) - progressPct = reportProgress(progressPct, kStage1Frac); //proportion correct, 0..100 - // struct TDICOMdata dcmList [nameList.numItems]; //<- this exhausts the stack for large arrays + progressPct = reportProgress(progressPct, kStage1Frac); //proportion correct, 0..100 // struct TDICOMdata dcmList [nameList.numItems]; //<- this exhausts the stack for large arrays struct TDICOMdata *dcmList = (struct TDICOMdata *)malloc(nameList.numItems * sizeof(struct TDICOMdata)); struct TDTI4D *dti4D = (struct TDTI4D *)malloc(sizeof(struct TDTI4D)); struct TDCMprefs prefs; @@ -8183,7 +8491,7 @@ int nii_loadDirCore(char *indir, struct TDCMopts *opts) { printMessage("Stage 2 (Read DICOM headers, Convert 4D) required %f seconds.\n", ((float)(clock() - start)) / CLOCKS_PER_SEC); start = clock(); #endif - if (opts->isRenameNotConvert) { + if ((opts->isRenameNotConvert) || (opts->onlySearchDirForDICOM != 0)) { free(dcmList); free(dti4D); return EXIT_SUCCESS; @@ -8251,7 +8559,7 @@ int nii_loadDirCore(char *indir, struct TDCMopts *opts) { fillTDCMsort(dcmSort[nConvert], j, dcmList[j]); nConvert++; } else { - if (isNonParallelSlices) { + if (isNonParallelSlices) { dcmList[i].isNonParallelSlices = true; dcmList[j].isNonParallelSlices = true; } @@ -8280,7 +8588,7 @@ int nii_loadDirCore(char *indir, struct TDCMopts *opts) { free(dcmSort); } //convert all images of this series } -#else //avoid bubble sort - dont check all images for match, only those with identical series instance UID +#else //avoid bubble sort - do not check all images for match, only those with identical series instance UID //3: stack DICOMs with the same Series struct TWarnings warnings = setWarnings(); //sort by series instance UID ... avoids bubble-sort penalty @@ -8320,6 +8628,8 @@ int nii_loadDirCore(char *indir, struct TDCMopts *opts) { nConvert++; } } //for all images with same seriesUID as first one + + // MGH set Opts.isForceStackSameSeries = 1 by default, isMultiEcho, isNonParallelSlices, isCoilVaries remain false for MGH default run after isSameSet if ((isNonParallelSlices) && (dcmList[ii].CSA.mosaicSlices > 1) && (nConvert > 0)) { //issue481: if ANY volumes are non-parallel, save ALL as 3D printWarning("Saving mosaics with non-parallel slices as 3D (issue 481)\n"); for (int j = i; j < (int)nDcm; j++) { @@ -8596,27 +8906,44 @@ int findpathof(char *pth, const char *exe) { void readFindPigz(struct TDCMopts *opts, const char *argv[]) { #if defined(_WIN64) || defined(_WIN32) strcpy(opts->pigzname, "pigz.exe"); + if (is_exe(opts->pigzname)) + return; if (!is_exe(opts->pigzname)) { -#if defined(__APPLE__) -#ifdef myDisableZLib - printMessage("Compression requires %s in the same folder as the executable http://macappstore.org/pigz/\n", opts->pigzname); -#else //myUseZLib - if (opts->isVerbose > 0) - printMessage("Compression will be faster with %s in the same folder as the executable http://macappstore.org/pigz/\n", opts->pigzname); -#endif - strcpy(opts->pigzname, ""); -#else + char exepth[PATH_MAX]; + strcpy(exepth, argv[0]); + dropFilenameFromPath(exepth); //, opts.pigzname); + char appendChar[2] = {"a"}; + appendChar[0] = kPathSeparator; + strcat(exepth, appendChar); + strcat(exepth, opts->pigzname); + strcpy(opts->pigzname, exepth); + } + if (is_exe(opts->pigzname)) + return; + HMODULE hModule = GetModuleHandle(NULL); + if (hModule != NULL) { + // https://stackoverflow.com/questions/1528298/get-path-of-executable + char exepth[PATH_MAX]; + GetModuleFileName(hModule, exepth, (sizeof(exepth))); + dropFilenameFromPath(exepth); //, opts.pigzname); + char appendChar[2] = {"a"}; + appendChar[0] = kPathSeparator; + strcat(exepth, appendChar); + strcpy(opts->pigzname, "pigz.exe"); + strcat(exepth, opts->pigzname); + strcpy(opts->pigzname, exepth); + } + if (is_exe(opts->pigzname)) + return; #ifdef myDisableZLib printMessage("Compression requires %s in the same folder as the executable\n", opts->pigzname); #else //myUseZLib if (opts->isVerbose > 0) printMessage("Compression will be faster with %s in the same folder as the executable\n", opts->pigzname); #endif - strcpy(opts->pigzname, ""); -#endif - } else - strcpy(opts->pigzname, ".\\pigz"); //drop -#else + strcpy(opts->pigzname, ""); + return; +#else //if windows else linux char str[PATH_MAX]; //possible pigz names const char *names[] = { @@ -8718,11 +9045,13 @@ void setDefaultOpts(struct TDCMopts *opts, const char *argv[]) { //either "setDe opts->isSaveNativeEndian = true; opts->isAddNamePostFixes = true; //e.g. "_e2" added for second echo opts->isTestx0021x105E = false; //GE test slice times stored in 0021,105E + opts->diffCyclingModeGE = -1; opts->isIgnoreTriggerTimes = 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; + opts->onlySearchDirForDICOM = 0; opts->isProgress = 0; opts->nameConflictBehavior = kNAME_CONFLICT_ADD_SUFFIX; #ifdef myDisableZLib @@ -8752,6 +9081,8 @@ void setDefaultOpts(struct TDCMopts *opts, const char *argv[]) { //either "setDe opts->numSeries = 0; memset(opts->seriesNumber, 0, sizeof(opts->seriesNumber)); strcpy(opts->filename, "%f_%p_%t_%s"); + + opts->isDumpNotConvert = false; } // setDefaultOpts() #if defined(_WIN64) || defined(_WIN32) @@ -8802,11 +9133,11 @@ void readIniFile(struct TDCMopts *opts, const char *argv[]) { void readIniFile(struct TDCMopts *opts, const char *argv[]) { setDefaultOpts(opts, argv); - sprintf(opts->optsname, "%s%s", getenv("HOME"), STATUSFILENAME); + snprintf(opts->optsname, kOptsStr, "%s%s", getenv("HOME"), STATUSFILENAME); FILE *fp = fopen(opts->optsname, "r"); if (fp == NULL) return; - char Setting[20], Value[255]; + char Setting[255], Value[255]; //while ( fscanf(fp, "%[^=]=%s\n", Setting, Value) == 2 ) { //while ( fscanf(fp, "%[^=]=%s\n", Setting, Value) == 2 ) { while (fscanf(fp, "%[^=]=%[^\n]\n", Setting, Value) == 2) { diff --git a/console/nii_dicom_batch.h b/console/nii_dicom_batch.h index 80902c0d..00ab73ec 100644 --- a/console/nii_dicom_batch.h +++ b/console/nii_dicom_batch.h @@ -25,15 +25,15 @@ extern "C" { typedef struct { - struct nifti_1_header hdr0; + struct nifti_1_header hdr0; - size_t imgsz; - unsigned char *imgM; + size_t imgsz; + unsigned char *imgM; - struct TDICOMdata tdicomData; + struct TDICOMdata tdicomData; - struct TDTI *tdti; - int numDti; + struct TDTI *tdti; + int numDti; } MRIFSSTRUCT; MRIFSSTRUCT* nii_getMrifsStruct(); @@ -54,11 +54,13 @@ void nii_clrMrifsStruct(); #define kSaveFormatBNII 4 #define MAX_NUM_SERIES 16 +#define kOptsStr 512 struct TDCMopts { + bool isDumpNotConvert; 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]; + int saveFormat, isMaximize16BitRange, isForceStackSameSeries, nameConflictBehavior, isVerbose, isProgress, compressFlag, dirSearchDepth, onlySearchDirForDICOM, gzLevel, diffCyclingModeGE; //support for compressed data 0=none, + char filename[kOptsStr], outdir[kOptsStr], indir[kOptsStr], pigzname[kOptsStr], optsname[kOptsStr], indirParent[kOptsStr], imageComments[24]; double seriesNumber[MAX_NUM_SERIES]; //requires double must store -1 (report but do not convert) as well as seriesUidCrc (uint32) long numSeries; #ifdef USING_R @@ -77,7 +79,7 @@ void nii_clrMrifsStruct(); void readIniFile (struct TDCMopts *opts, const char * argv[]); int nii_saveNIIx(char * niiFilename, struct nifti_1_header hdr, unsigned char* im, struct TDCMopts opts); int nii_loadDir(struct TDCMopts *opts); - int nii_loadDirCore(char *indir, struct TDCMopts* opts); + int nii_loadDirCore(char *indir, struct TDCMopts* opts); 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); diff --git a/console/nii_foreign.cpp b/console/nii_foreign.cpp index 5547a58c..8377160c 100644 --- a/console/nii_foreign.cpp +++ b/console/nii_foreign.cpp @@ -399,7 +399,7 @@ unsigned char *readEcat7(const char *fname, struct TDICOMdata *dcm, struct nifti } dcm->manufacturer = kMANUFACTURER_SIEMENS; //dcm->manufacturersModelName = itoa(mhdr.system_type); - sprintf(dcm->manufacturersModelName, "%d", mhdr.system_type); + snprintf(dcm->manufacturersModelName, kDICOMStr, "%d", mhdr.system_type); dcm->bitsAllocated = bytesPerVoxel * 8; if (isScaleFactorVaries) dcm->isFloat = true; diff --git a/console/ucm.cmake b/console/ucm.cmake index e6e7fc8f..37f025f3 100644 --- a/console/ucm.cmake +++ b/console/ucm.cmake @@ -611,7 +611,7 @@ macro(ucm_add_target) # also set the name of the target output as the original one set_target_properties(${unity_target_name} PROPERTIES OUTPUT_NAME ${ARG_NAME}) if(UCM_NO_COTIRE_FOLDER) - # reset the folder property so all unity targets dont end up in a single folder in the solution explorer of VS + # reset the folder property so all unity targets do not end up in a single folder in the solution explorer of VS set_target_properties(${unity_target_name} PROPERTIES FOLDER "") endif() set_target_properties(all_unity PROPERTIES FOLDER "CMakePredefinedTargets") diff --git a/console/windows.bat b/console/windows.bat new file mode 100644 index 00000000..acc18462 --- /dev/null +++ b/console/windows.bat @@ -0,0 +1,4 @@ +cl /wd4018 /wd4068 /wd4101 /wd4244 /wd4267 /wd4305 /wd4308 /wd4334 /wd4800 /wd4819 /wd4996 base64.cpp cJSON.cpp main_console.cpp nii_foreign.cpp nii_dicom.cpp jpg_0XC3.cpp ujpeg.cpp nifti1_io_core.cpp nii_ortho.cpp nii_dicom_batch.cpp /Fe:dcm2niix.exe -DmyDisableOpenJPEG /link /STACK:16388608 +rm *.exp +rm *.lib +rm *.obj \ No newline at end of file diff --git a/dcm2niix/__init__.py b/dcm2niix/__init__.py new file mode 100644 index 00000000..8a4127ab --- /dev/null +++ b/dcm2niix/__init__.py @@ -0,0 +1,27 @@ +"""Thin wrapper around dcm2niix binary""" +__author__ = "Casper da Costa-Luis " +__date__ = "2022" +# version detector. Precedence: installed dist, git, 'UNKNOWN' +try: + from ._dist_ver import __version__ +except ImportError: # pragma: nocover + try: + from setuptools_scm import get_version + + __version__ = get_version(root="../..", relative_to=__file__) + except (ImportError, LookupError): + __version__ = "UNKNOWN" +__all__ = ['bin', 'bin_path', 'main'] + +from pathlib import Path + +bin_path = Path(__file__).resolve().parent / "dcm2niix" +bin = str(bin_path) + + +def main(args=None): + if args is None: + import sys + args = sys.argv[1:] + from subprocess import run + run([bin] + args) diff --git a/dcm2niix/__main__.py b/dcm2niix/__main__.py new file mode 100644 index 00000000..8273c4ff --- /dev/null +++ b/dcm2niix/__main__.py @@ -0,0 +1,3 @@ +from . import main + +main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..5c9a0639 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,7 @@ +[build-system] +requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4", + "scikit-build>=0.11.0", "cmake>=3.18", "ninja"] + +[tool.setuptools_scm] +write_to = "dcm2niix/_dist_ver.py" +write_to_template = "__version__ = '{version}'\n" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..785be9d2 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,41 @@ +[metadata] +name=dcm2niix +description=DCM2NIIX Python package +long_description=file: README.md +long_description_content_type=text/markdown +license_file=license.txt +url=https://github.com/rordenlab/dcm2niix +project_urls= + Changelog=https://github.com/rordenlab/dcm2niix/releases + Documentation=https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage +author=Li X, Morgan PS, Ashburner J, Smith J, Rorden C +maintainer=Casper da Costa-Luis +maintainer_email=imaging@cdcl.ml +keywords=research, jpeg, dicom, neuroscience, mri, neuroimaging, nifti, dcm, nii, nitrc, bids, dcm2niix, mricrogl +classifiers= + Development Status :: 5 - Production/Stable + Intended Audience :: Education + Intended Audience :: Healthcare Industry + Intended Audience :: Science/Research + Operating System :: Microsoft :: Windows + Operating System :: POSIX :: Linux + Programming Language :: C++ + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3 :: Only + Topic :: Scientific/Engineering :: Medical Science Apps. +[options] +setup_requires= + setuptools>=42 + wheel + setuptools_scm[toml] + scikit-build>=0.11.0 + cmake>=3.18 + ninja +python_requires=>=3.6 +[options.entry_points] +console_scripts= + dcm2niix=dcm2niix:main diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..146f7a03 --- /dev/null +++ b/setup.py @@ -0,0 +1,13 @@ +"""Compile source code and setup Python 3 package""" +import re +from pathlib import Path + +from setuptools_scm import get_version +from skbuild import setup + +__version__ = get_version(root=".", relative_to=__file__) +build_ver = ".".join(__version__.split(".")[:3]).split(".dev")[0] +for i in (Path(__file__).resolve().parent / "_skbuild").rglob("CMakeCache.txt"): + i.write_text(re.sub("^//.*$\n^[^#].*pip-build-env.*$", "", i.read_text(), flags=re.M)) +setup(use_scm_version=True, packages=["dcm2niix"], + cmake_languages=("CXX",), cmake_minimum_required_version="3.18")