diff --git a/.travis.yml b/.travis.yml index 70a5c92a..8775c604 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,9 +22,7 @@ matrix: before_install: - eval "${MATRIX_EVAL}" - - git submodule update --init --depth=3 dcm_qa - - git submodule update --init --depth=3 dcm_qa_nih - - git submodule update --init --depth=3 dcm_qa_uih + - 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 - diff --git a/Canon/README.md b/Canon/README.md index fdf51e98..7ee79845 100644 --- a/Canon/README.md +++ b/Canon/README.md @@ -2,17 +2,27 @@ dcm2niix can convert Canon (né Toshiba) DICOM format images to NIfTI. This page notes vendor specific conversion details. +## Avoid Classic DICOM + +Users of Canon MRI equipment are strongly advised to export data from their scanners as enhanced DICOM (with all images from the series stored as a single file) rather than classic DICOM (each 2D slice stored as a separate file). Limitations of the Canon classic DICOMs are described [here](https://github.com/rordenlab/dcm2niix/issues/495) and [here](https://github.com/neurolabusc/dcm_qa_canon). + ## Diffusion Weighted Imaging Notes -In contrast to several other vendors, Toshiba used public tags to report diffusion properties. Specifically, [DiffusionBValue (0018,9087)](http://dicomlookup.com/lookup.asp?sw=Tnumber&q=(0018,9087)) and [DiffusionGradientOrientation (0018,9089)](http://dicomlookup.com/lookup.asp?sw=Tnumber&q=(0018,9089)). Be aware that these tags are only populated for images where a diffusion gradient is applied. Consider a typical diffusion series where some volumes are acquired with B=0 while others have B=1000. In this case, only the volumes with B>0 will report a DiffusionBValue. +In contrast to several other vendors, Toshiba used public tags to report diffusion properties. Specifically, [DiffusionBValue (0018,9087)](http://dicomlookup.com/lookup.asp?sw=Tnumber&q=(0018,9087)) and [DiffusionGradientOrientation (0018,9089)](http://dicomlookup.com/lookup.asp?sw=Tnumber&q=(0018,9089)). Be aware that these tags are only populated for images where a diffusion gradient is applied. Consider a typical diffusion series where some volumes are acquired with B=0 while others have B=1000. In this case, only the volumes with B>0 will report a DiffusionBValue. These coordinates are with respect to the scanner bore, not image space. -Since the acquisition by Canon, these public tags are no longer populated. The diffusion gradient directions are now stored in the ASCII Image Comments tag. Like GE (but unlike [Siemens, GE and Toshiba](https://www.na-mic.org/wiki/NAMIC_Wiki:DTI:DICOM_for_DWI_and_DTI)), these directions are with respect to the image space, not the scanner bore. For empirical data see the Sample Datasets section. +Since the acquisition by Canon, these public tags are no longer populated for images saved in classic 2D DICOM format. The diffusion gradient directions are now stored in the ASCII Image Comments tag. Like GE (but unlike [Siemens, GE and Toshiba](https://www.na-mic.org/wiki/NAMIC_Wiki:DTI:DICOM_for_DWI_and_DTI)), these directions are with respect to the image space, not the scanner bore. Further, gradient direction is not adjusted for phase encoding polarity, and it is impossible to determine phase encoding polarity. For detailed discussion and a validation dataset that exhibits these attributes please see [dcm_qa_canon](https://github.com/neurolabusc/dcm_qa_canon). A Canon classic DICOM DWI image may report: ``` (0018,9087) FD 1500 # 8, 1 DiffusionBValue (0020,4000) LT [b=1500(0.445,0.000,0.895)] # 26, 1 ImageComments ``` +In contrast, when exporting images as enhanced (4D) DICOM, information is stored in public tags and does appear to compensate for phase encode polarity. These coordinates are with respect to the scanner bore, not image space. A Canon classic DICOM DWI image may report: + +``` +(0018,9087) FD 1500 # 8, 1 DiffusionBValue +(0018,9089) FD 0.29387456178665161\-0.95365142822265625\-0.064700603485107422 # 24, 3 DiffusionGradientOrientation +``` ## Unknown Properties diff --git a/FILENAMING.md b/FILENAMING.md index 620f9030..e1090ab8 100644 --- a/FILENAMING.md +++ b/FILENAMING.md @@ -12,6 +12,7 @@ You request the output file name with the `-f` argument. For example, consider y - %d=description (from 0008,103E) - %e=echo number (from 0018,0086) - %f=folder name (name of folder containing first DICOM) + - %g=accession number (0008,0050) - %i=ID of patient (from 0010,0020) - %j=series instance UID (from 0020,000E) - %k=study instance UID (from 0020,000D) @@ -19,7 +20,7 @@ You request the output file name with the `-f` argument. For example, consider y - %m=manufacturer short name (from 0008,0070: GE, Ph, Si, To, UI, NA) - %n=name of patient (from 0010,0010) - %o=mediaObjectInstanceUID (0002,0003)* - - %p=protocol name (from 0018,1030). If 0018,1030 is empty, or if the Manufacturer (0008,0070) is GE with Modality (0008,0060) of MR, then the SequenceName (0018,0024) is used if it is not empty. + - %p=protocol name (from 0018,1030). If 0018,1030 is empty, the SequenceName (0018,0024) is used. - %r=instance number (from 0020,0013)* - %s=series number (from 0020,0011) - %t=time of study (from 0008,0020 and 0008,0030) @@ -68,9 +69,9 @@ dcm2niix will attempt to write your image using the naming scheme you specify wi ## Special Characters -[Some characters are not permitted](https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names) in file names. The following characters will be replaced with underscorces (`_`). Note that the forbidden characters vary between operating systems (Linux only forbids the forward slash, MacOS forbids forward slash and colon, while Windows forbids any of the characters listed below). To ensure that files can be easily copied between file systems, [dcm2niix restricts file names to characters allowed by Windows](https://github.com/rordenlab/dcm2niix/issues/237). +[Some characters are not permitted](https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names) in file names. The following characters will be replaced with underscorces (`_`). Note that the forbidden characters vary between operating systems (Linux only forbids the forward slash, MacOS forbids forward slash and colon, while Windows forbids any of the characters listed below). To ensure that files can be easily copied between file systems, [dcm2niix restricts file names to characters allowed by Windows](https://github.com/rordenlab/dcm2niix/issues/237). While technically legal in all filesystems, the semicolon can wreak havoc in [Windows](https://stackoverflow.com/questions/3869594/semi-colons-in-windows-filenames) and [Linux](https://forums.plex.tv/t/linux-hates-semicolons-in-file-names/49098/2). -### List of Forbidden Characters (based on Windows) +### List of Forbidden Characters ``` < (less than) > (greater than) @@ -81,6 +82,7 @@ dcm2niix will attempt to write your image using the naming scheme you specify wi | (vertical bar or pipe) ? (question mark) * (asterisk) +; (semicolon) ``` [Control characters](https://en.wikipedia.org/wiki/ASCII#Control_characters) like backspace and tab are also forbidden. diff --git a/GE/README.md b/GE/README.md index 03453129..6f6b8226 100644 --- a/GE/README.md +++ b/GE/README.md @@ -34,15 +34,17 @@ Some sequences allow the user to interpolate images in plane (e.g. saving a 2D 6 ## Total Readout Time -One often wants to determine [echo spacing, bandwidth](https://support.brainvoyager.com/brainvoyager/functional-analysis-preparation/29-pre-processing/78-epi-distortion-correction-echo-spacing-and-bandwidth) and total read-out time for EPI data so they can be undistorted. Speifically, we are interested in FSL's definition of total read-out time, which may differ from the actual read-out time. FSL expects “the time from the middle of the first each to the middle of the last echo, as it would have been had partial k-space not been used”. So total read-out time is influenced by parallel acceleration factor, bandwidth, number of EPI lines, but not partial Fourier. For GE data we can use the Acquisition Matrix (0018,1310) in the phase-encoding direction, the in-plane acceleration ASSET R factor (the reciprocal of this is stored as the first element of 0043,1083) and the Effective Echo Spacing (0043,102C). While GE does not tell us the [partial Fourier fraction](https://bids-specification.readthedocs.io/en/stable/04-modality-specific-files/01-magnetic-resonance-imaging-data.html), is does reveal if it is present with the ScanOptions (0018,1022) reporting [PFF](http://dicomlookup.com/lookup.asp?sw=Ttable&q=C.8-4) (in my experience, GE does not populate [(0018,9081)](http://dicomlookup.com/lookup.asp?sw=Tnumber&q=(0018,9081))). While partial Fourier does not impact FSL's totalReadoutTime directly, it can interact with the number of lines acquired when combined with parallel imaging (the `Round_factor` 2 (Full Fourier) or 4 (Partial Fourier)). +One often wants to determine [echo spacing, bandwidth](https://support.brainvoyager.com/brainvoyager/functional-analysis-preparation/29-pre-processing/78-epi-distortion-correction-echo-spacing-and-bandwidth) and total read-out time for EPI data so they can be undistorted. Specifically, we are interested in FSL's definition of total read-out time, which may differ from the actual read-out time. FSL expects “the time from the middle of the first echo to the middle of the last echo, as it would have been had partial k-space not been used”. So total read-out time is influenced by parallel acceleration factor, bandwidth, number of EPI lines, but not partial Fourier. For GE data we can use the Acquisition Matrix (0018,1310) in the phase-encoding direction, the in-plane acceleration ASSET R factor (the reciprocal of this is stored as the first element of 0043,1083) and the Effective Echo Spacing (0043,102C). While GE does not tell us the [partial Fourier fraction](https://bids-specification.readthedocs.io/en/stable/04-modality-specific-files/01-magnetic-resonance-imaging-data.html), is does reveal if it is present with the ScanOptions (0018,1022) reporting [PFF](http://dicomlookup.com/lookup.asp?sw=Ttable&q=C.8-4) (in my experience, GE does not populate [(0018,9081)](http://dicomlookup.com/lookup.asp?sw=Tnumber&q=(0018,9081))). While partial Fourier does not impact FSL's totalReadoutTime directly, it can interact with the number of lines acquired when combined with parallel imaging (the `Round_factor` 2 (Full Fourier) or 4 (Partial Fourier)). The formula for FSL's definition of TotalReadoutTime (in seconds) is: ``` TotalReadoutTime = ( ( ceil ((1/Round_factor) * PE_AcquisitionMatrix / Asset_R_factor ) * Round_factor) - 1 ] * EchoSpacing * 0.000001 +EffectiveEchoSpacing = TotalReadoutTime/ (reconMatrixPE - 1) ``` Consider an example: + ``` (0018,1310) US 128\0\0\128 # 8, 4 AcquisitionMatrix (0018,0022) CS [SAT_GEMS\MP_GEMS\EPI_GEMS\ACC_GEMS\PFF\FS] # 42, 6 ScanOptions diff --git a/Philips/README.md b/Philips/README.md index 6813e9b6..97fa3138 100644 --- a/Philips/README.md +++ b/Philips/README.md @@ -1,6 +1,6 @@ ## About -dcm2niix attempts to convert all DICOM images to NIfTI. The Philips enhanced DICOM images are elegantly able to save all images from a series as a single file. However, this format is necessarily complex. The usage of this format has evolved over time, and can become further complicated when DICOM are handled by DICOM tools (for example, anonymization, transfer which converts explicit VRs to implicit VRs, etc.). +dcm2niix attempts to convert all DICOM images to NIfTI. The Philips enhanced DICOM images are elegantly able to save all images from a series as a single file. However, this format is necessarily complex. The usage of this format has evolved over time, and can become further complicated when DICOM are modified by DICOM tools (for example, anonymization, mangled by a [dcm4che/AGFA PACS](https://github.com/neurolabusc/dcm_qa_agfa), conversion of explicit VRs to implicit VRs, etc.). This web page describes some of the strategies handle these images. However, users should be vigilant when handling these datasets. If you encounter problems using dcm2niix you can explore [alternative DICOM to NIfTI converters](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Alternatives) or [report an issue](https://github.com/rordenlab/dcm2niix). @@ -18,24 +18,62 @@ Therefore, dcm2niix will ignore the IPP enclosed in 2005,140F unless no alternat ## Image Scaling -dcm2niix losslessy copies the raw data from DICOM to NIfTI format. These values are typically stored as 16-bit integers in the range -32768..32767. Both the DICOM and NIfTI formats describe how scaling intercept and slope values can be used to convert these raw values into calibrated values. For example, with an intercept of 0 and slope of 0.01 the raw value of 50 would be converted to 0.5. +How data is represented in DICOM for MR has several challenges and the technology and standard has evolved over the years to accommodate new uses. Unlike CT, where the signal is naturally displayed in Hounsfield units, MR has no natural signal units and the magnitude is influenced by the electronics and the software processing required to bring this to the final image. Secondly most of the original DICOM implementations used small bit number integers to store the underlying images for economy of storage. As a result it is necessary to apply scaling from the internal DICOM storage to a form suitable for radiographic display or quantitative measurement. There remain several challenges with this process, ensuring that the mapping to the integer values makes best use of the available bit depth for images with large dynamic range, or large changes between images, without clipping the data while also preserving the appearance of the noise field which is demanded by the needs of radiographic visual review. Note that for most MRI modalities these concerns do not impact analyses: the intensity is assumed arbitrary, the statistics treat signal offset and scaling as nuisance regressors when fitting models, and cacluations are computed with high precision floating point numbers. However, there are some situations such as arterial spin labeling where image scaling is important. In these situations, scaling is a crucial aspect to be aware of for quantitative methods and which representation is used depends upon your needs. -Unlike other vendors, Philips can store different scaling factors in their DICOM header. For most MRI modalities where the intensity brightness is relative, this has no impact. However, for modalities like ASL it can have an impact. The NIfTI format requires a single intensity intercept and slope is chosen. Therefore, dcm2niix will choose the "Real World" values if provided. If these are not available, dcm2niix will choose either the "precise" (default) or "display" (if the user choose "-p n") value. dcm2niix will also populate the folllowing tags in the BIDS header that allow the user to select between different intensity scaling formats: "PhilipsRescaleSlope", "PhilipsRescaleIntercept", "PhilipsScaleSlope", "UsePhilipsFloatNotDisplayScaling" (where "1" indicates NIfTI uses precise value, and "0" indicates display values)., "PhilipsRWVSlope" and "PhilipsRWVIntercept". +At its simplest image scaling requires a rescale slope and intercept defined by the DICOM standard tags [0028,1053](http://dicomlookup.com/lookup.asp?sw=Tnumber&q=(0028,1053)) and [0028,1052](http://dicomlookup.com/lookup.asp?sw=Tnumber&q=(0028,1053)). Whether these values are the same for all images, or image specific depends upon the implementation and potentially the location of these tags withing the DICOM tag structure. For manufacturers other than Philips, these are the only intensity scaling values provided, so there is no concern regarding which scaling values should be used. -The relevant DICOM tags are -RS = rescale slope ([0028,1053](http://dicomlookup.com/lookup.asp?sw=Tnumber&q=(0028,1053))) -RI = rescale intercept ([0028,1052](http://dicomlookup.com/lookup.asp?sw=Tnumber&q=(0028,1052))) -SS = scale slope (2005,100E) -RealWorldIntercept = (0040,9224) -Real World Slope = (0040,9225) -The transformation formulas are: -R = raw value, P = precise value, D = displayed value -D = R * RS + RI -P = D/(RS * SS) +However, the DICOM standard introduced the concept of [`real world units`](http://dicom.nema.org/dicom/2013/output/chtml/part03/sect_A.46.html). This allows the storage of one or more mappings to allow selective viewing of the data mapped into different value ranges (which may also be non-linear mappings). + +Philips thinks in terms of three different representations (using the terminology of the documentation available to Philips collaborators): + +| Name | ID | Description| +| ---------------- | ------------- | ------------- | +| Stored Value | SV | Raw data stored in DICOM tag PIXEL DATA tag (7FE0,0010)| +| Displayed Value | DV | The value which is shown to the user when using scanner interface, ROIS, measurements etc. | +| Floating Point | FP | An internal value at a point earlier in the reconstruction chain before the conversion to DICOM/integer for image presentation. | +| Real World Value | WV | DICOM defined real world units| + +In general SV should not be used for quantitative measurements as it is an integer format. In practice, if the Rescale values are the same for all images (the typical case, but not guaranteed) SV can be used to compare signal intensities between images from the same scan. Note that the NIfTI format only provides a single `scl_slope` and `scl_inter` for the entire file, whereas in DICOM rescale values can in theory differ across 2D slices. Therefore, in situations where the rescale values do differ across slices, dcm2niix will apply the requested rescale to each slice and save the scaled data as the 32-bit float NIfTI dataset. This preserves the varibility reported by the rescale tags, at the cost of disk space. + +DV can be used for quantitative comparison of signal intensities between images in the same scan as long as the relevant rescale values are taken into account. These rescale values may come from the tags standard tags 0028,1053 and 0028,1052 or from a relevant RealWorld block if present. If the DV is derived from a RealWorld block with defined units (tag (0008,0104) such as Hz or ms rather than “no units”) or a RescaleType (0028,1054) with a non-US type (not defined by the standard), then the DV is already quantitative and cross scan comparison may be done. + +However, in general DV is not sufficient to compare images from different scans, especially if the signal intensity varies a lot (eg multiple inversion recovery scans) in which case the FP value may be used as this may be compared (with some caveats) across scans and across timescales. This scaling requires an additional scale factor on top of the DV value, the Scale Slope (private tag (2005,100E)) + +As long as rescale values are identical across all DICOM slices, dcm2niix losslessly copies the raw pixel data from the DICOM tag (7FE0,0010) to NIfTI image. These values are typically stored as 16-bit integers in the range -32768..32767. Both the DICOM and NIfTI formats describe how scaling intercept and slope values can be used to convert these raw values into calibrated values. For example, with an intercept of 0 and slope of 0.01 the raw value of 50 would be converted to 0.5. + +The [NIfTI](https://nifti.nimh.nih.gov/pub/dist/src/niftilib/nifti1.h) header provides the `scl_slope` and `scl_inter` fields so each voxel value in the dataset is scaled as: + +``` +I = scl_slope * SV + scl_inter +``` + +where `SV` is the raw stored value and `I` is the "true" transformed voxel intensity. + +Philips has three possible intensity transforms for their DICOM images (world (`W`), display (`D`), precise (`P`)). All of these transforms might be provided in a single DICOM image, while the [NIfTI](https://nifti.nimh.nih.gov/pub/dist/src/niftilib/nifti1.h) header only designates a single `scl_slope` and `scl_inter` for each image. dcm2niix will attempt to retain the stored values (`SV`) and sets the NIfTI `scl_inter` and `scl_slope values` for the desired intensity transform. dcm2niix will use `FP` if possible. If this is not possibleor the user specifies `-p n` dcm2niix will use the transforms for `DV`. + +The formulas are provided below. The DICOM tags are in brackets (e.g. `(0040,9225)`) and the BIDS tag is in double quotes (e.g. `"PhilipsRWVSlope"`). Since all the scaling values are stored in the BIDS sidecar, you can always use these to later your preferred intensity transform (assume all slices used the same scaling values). + +``` +Inputs: + SV = stored value of DICOM PIXEL DATA without scaling + WS = RealWorldValue slope (0040,9225) "PhilipsRWVSlope" + WI = RealWorldValue intercept (0040,9224) "PhilipsRWVIntercept" + RS = rescale slope (0028,1053) "PhilipsRescaleSlope" + RI = rescale intercept (0028,1052) "PhilipsRescaleIntercept" + SS = scale slope (2005,100E) "PhilipsScaleSlope" +Outputs: + WV = real world value + FP = precise value + DV = displayed value +Formulas: + WV = SV * WS + WI + DV = SV * RS + RI + FP = DV / (RS * SS) +``` ## Derived parametric maps stored with raw diffusion data -Some Philips diffusion DICOM images include derived image(s) along with the images. Other manufacturers save these derived images as a separate series number, and the DICOM standard seems ambiguous on whether it is allowable to mix raw and derived data in the same series (see PS 3.3-2008, C.7.6.1.1.2-3). In practice, many Philips diffusion images append [derived parametric maps](http://www.revisemri.com/blog/2008/diffusion-tensor-imaging/) with the original data. With Philips, appending the derived isotropic image is optional - it is only created for the 'clinical' DTI schemes for radiography analysis and is triggered if the first three vectors in the gradient table are the unit X,Y and Z vectors. For conventional DWI, the result is the conventional mean of the ADC X,Y,Z for DTI it the conventional mean of the 3 principle Eigen vectors. As scientists, we want to discard these derived images, as they will disrupt data processing and we can generate better parametric maps after we have applied undistortion methods such as [Eddy and Topup](https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/eddy/UsersGuide). The current version of dcm2niix uses the Diffusion Directionality (0018,9075) tag to detect B=0 unweighted ("NONE"), B-weighted ("DIRECTIONAL"), and derived ("ISOTROPIC") images. Note that the Dimension Index Values (0020,9157) tag provides an alternative approach to discriminate these images. Here are sample tags from a Philips enhanced image that includes and derived map (3rd dimension is "1" while the other images set this to "2"). +Some Philips diffusion DICOM images include derived image(s) along with the images. Other manufacturers save these derived images as a separate series number, and the DICOM standard seems ambiguous on whether it is allowable to mix raw and derived data in the same series (see PS 3.3-2008, C.7.6.1.1.2-3). In practice, many Philips diffusion images append [derived parametric maps](http://www.revisemri.com/blog/2008/diffusion-tensor-imaging/) with the original data. With Philips, appending the derived isotropic image is optional - it is only created for the 'clinical' DTI schemes for radiography analysis and is triggered if the first three vectors in the gradient table are the unit X,Y and Z vectors. For conventional DWI, the result is the conventional mean of the ADC X,Y,Z for DTI it the conventional mean of the 3 principle Eigen vectors. As scientists, we want to discard these derived images, as they will disrupt data processing and we can generate better parametric maps after we have applied undistortion methods such as [Eddy and Topup](https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/eddy/UsersGuide). The current version of dcm2niix uses the Diffusion Directionality (0018,9075) tag to detect B=0 unweighted ("NONE"), B-weighted ("DIRECTIONAL"), and derived ("ISOTROPIC") images. Note that the Dimension Index Values (0020,9157) tag provides an alternative approach to discriminate these images. Here are sample tags from a Philips enhanced image that includes and derived map (3rd dimension is "1" while the other images set this to "2"). ``` (0018,9075) CS [DIRECTIONAL] @@ -102,7 +140,7 @@ Another value desirable for TOPUP is the "TotalReadoutTime". Again, one can not ## Partial Volumes -NIfTI expects all 3D volumes of a 4D series to have the same number of series (e.g. a time series of 3D fMRI volumes, or a diffusion set with 3D volumes with different gradients applied). If a fMRI sequence is aborted part way through, it is possible that a Philips scanner will only save part of the final volume. An example would be where the total slices (9970) does not equal Dynamics (290) x slices (35) = 10150. Current versions of dcm2niix expect complete volumes. You can repair your data using the console or a Python script, as discussed in [issue 357](https://github.com/rordenlab/dcm2niix/issues/357). To resolve this situation by hand you could also [rename](RENAMING.md) your DICOM files with a call like `./dcm2niix -r y -f %t/%s_%p_%4y_%2r.dcm ~/out 0020,0100`. In this example, the [`%4y`](FILENAMING.md) parameter adds the volume (Temporal Position, 0020,0100) to the filename, allowing you to identify volumes with missing slices. +NIfTI expects all 3D volumes of a 4D series to have the same number of series (e.g. a time series of 3D fMRI volumes, or a diffusion set with 3D volumes with different gradients applied). If a fMRI sequence is aborted part way through, it is possible that a Philips scanner will only save part of the final volume. An example would be where the total slices (9970) does not equal Dynamics (290) x slices (35) = 10150. Current versions of dcm2niix expect complete volumes. You can repair your data using the console or a Python script, as discussed in [issue 357](https://github.com/rordenlab/dcm2niix/issues/357). To resolve this situation by hand you could also [rename](RENAMING.md) your DICOM files with a call like `./dcm2niix -r y -f %t/%s_%p_%4y_%2r.dcm ~/out 0020,0100`. In this example, the [`%4y`](FILENAMING.md) parameter adds the volume (Temporal Position, 0020,0100) to the filename, allowing you to identify volumes with missing slices. ## Non-Image DICOMs @@ -125,5 +163,8 @@ Prior versions of dcm2niix used different methods to sort images. However, these ## Sample Datasets - [National Alliance for Medical Image Computing (NAMIC) samples](http://www.insight-journal.org/midas/collection/view/194) - - [Unusual Philips Examples](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Unusual_MRI). - - [Diffusion Examples](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Diffusion_Tensor_Imaging). \ No newline at end of file + - [Unusual Philips Examples (e.g. multi-echo)](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Unusual_MRI) + - [Archival samples](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Archival_MRI) + - [Diffusion Examples](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Diffusion_Tensor_Imaging) + - [Additional Diffusion Examples](https://github.com/neurolabusc/dcm_qa_philips) + - [Enhanced DICOMs](https://github.com/neurolabusc/dcm_qa_enh) \ No newline at end of file diff --git a/README.md b/README.md index 1ebe4d1b..45e63545 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ There are a couple ways to install dcm2niix - Run the following command to get the latest 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/dcm2niix_win.zip` - [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 you can run `brew install dcm2niix`. @@ -121,7 +122,7 @@ The following tools exploit dcm2niix - [abcd-dicom2bids](https://github.com/DCAN-Labs/abcd-dicom2bids) selectively downloads high quality ABCD datasets. - [autobids](https://github.com/khanlab/autobids) automates dcm2bids which uses dcm2niix. - [BiDirect_BIDS_Converter](https://github.com/wulms/BiDirect_BIDS_Converter) for conversion from DICOM to the BIDS standard. - - [BIDScoin](https://github.com/Donders-Institute/bidscoin) is a DICOM to BIDS converter with thorough [documentation](https://bircibrain.github.io/computingguide/docs/bids/bidscoin). + - [BIDScoin](https://github.com/Donders-Institute/bidscoin) is a DICOM to BIDS converter with a GUI and thorough [documentation](https://bidscoin.readthedocs.io). - [BIDS Toolbox](https://github.com/cardiff-brain-research-imaging-centre/bids-toolbox) is a web service for the creation and manipulation of BIDS datasets, using dcm2niix for importing DICOM data. - [birc-bids](https://github.com/bircibrain/birc-bids) provides a Docker/Singularity container with various BIDS conversion utilities. - [BOLD5000_autoencoder](https://github.com/nmningmei/BOLD5000_autoencoder) uses dcm2niix to pipe imaging data into an unsupervised machine learning algorithm. diff --git a/UIH/README.md b/UIH/README.md index 1c2f37a1..fbbf4fb7 100644 --- a/UIH/README.md +++ b/UIH/README.md @@ -12,7 +12,7 @@ UIH supports two ways of archiving the DWI/DTI and fMRI data. One way is one DIC Tag ID | Tag Name | VR | VM | Description | Sample -- | -- | -- | -- | -- | -- 0061,1002 | Generate Private | US | 1 | Flag to generate private format file | 1 -**0061,4002** | **FOV** | SH | 1 | FOV(mm) | 224*224 +0061,4002 | FOV | SH | 1 | FOV(mm) | 224*224 0065,1000 | MeasurmentUID | UL | 1 | Measurement UID of Protocol | 12547865 0065,1002 | ImageOrientationDisplayed | SH | 1 | Image Orientation Displayed | Sag or Sag>Cor 0065,1003 | ReceiveCoil | LO | 1 | Receive Coil Information | H 8 @@ -21,9 +21,9 @@ Tag ID | Tag Name | VR | VM | Description | Sample 0065,1006 | Slice Group ID | IS | 1 | Slice Group ID | 1 0065,1007 | Uprotocol | OB | 1 | Uprotocol value |   0065,1009 | BActualValue | FD | 1 | Actual B-Value from sequence | 1000.0 -**0065,100A** | **BUserValue** | FD | 1 | User Choose B-Value from UI | 1000.0 -**0065,100B** | **Block Size** | DS | 1 | Size of the paradigm/block | 10 -**0065,100C** | **Experimental status** | SH | 1 | fMRI | rest/active +0065,100A | BUserValue | FD | 1 | User Choose B-Value from UI | 1000.0 +0065,100B | Block Size | DS | 1 | Size of the paradigm/block | 10 +0065,100C | Experimental status | SH | 1 | fMRI | rest/active 0065,100D | Parallel Information | SH | 1 | ratio of parallel acquisition and acceleration |   0065,100F | Slice Position | SH | 1 | Slice location displayed on the screen | H23.4 0065,1011 | Sections | SH | 1 |   |   @@ -38,17 +38,17 @@ Tag ID | Tag Name | VR | VM | Description | Sample 0065,1029 | AcquisitionDuration | SH | 1 | Acquisition Duration | 0.03 0065,102B | ApplicationCategory | LT | 1 | Application names available | DTI\Func 0065,102C | RepeatitionIndex | IS | 1 |   | 0 -**0065,102D** | **SequenceDisplayName** | ST | 1 | Sequence display name | Epi_dti_b0 +0065,102D | SequenceDisplayName | ST | 1 | Sequence display name | Epi_dti_b0 0065,102E | NoiseDecovarFlag | LO | 1 | Noise decorrelation flag | PreWhite 0065,102F | ScaleFactor | FL | 1 | scale factor | 2.125 0065,1031 | MRSequenceVariant | SH | 1 | SequenceVariant |   0065,1032 | MRKSpaceFilter | SH | 1 | K space filter |   0065,1033 | MRTableMode | SH | 1 | Table mode | Fix 0065,1036 | MRDiscoParameter | OB | 1 |   |   -**0065,1037** | **MRDiffusionGradOrientation** | FD | 3 | Diffusion gradient orientation | 0\0\0 +0065,1037 | MRDiffusionGradOrientation | FD | 3 | Diffusion gradient orientation | 0\0\0 0065,1038 | MRPerfusionNoiseLevel | FD | 1 | epi_dwi/perfusion noise level | 40 0065,1039 | MRGradRange | SH | 6 | linear range of gradient | 0.0\157\0.0\157\0.0\125 -**0065,1050** | **MR Number Of Slice In Volume** | DS | 1 | Number Of Frames In a Volume,Columns of each frame: cols =ceil(sqrt(total)) ; Rows of each frame: rows =ceil(total/cols) ; appeared when image type (00080008) has VFRAME | 27 +0065,1050 | MR Number Of Slice In Volume | DS | 1 | Number Of Frames In a Volume,Columns of each frame: cols =ceil(sqrt(total)) ; Rows of each frame: rows =ceil(total/cols) ; appeared when image type (00080008) has VFRAME | 27 0065,1051 | MR VFrame Sequence | SQ | 1 | 1 |   ->0008,0022 | Acquisition Date | DA | 1 |   |   ->0008,0032 | Acquisition Time | TM | 1 |   |   diff --git a/console/main_console.cpp b/console/main_console.cpp index a049830e..8e91a324 100644 --- a/console/main_console.cpp +++ b/console/main_console.cpp @@ -88,7 +88,7 @@ void showHelp(const char * argv[], struct TDCMopts opts) { #else #define kQstr "" #endif - printf(" -f : filename (%%a=antenna (coil) name, %%b=basename, %%c=comments, %%d=description, %%e=echo number, %%f=folder name, %%i=ID of patient, %%j=seriesInstanceUID, %%k=studyInstanceUID, %%m=manufacturer, %%n=name of patient, %%o=mediaObjectInstanceUID, %%p=protocol,%s %%r=instance number, %%s=series number, %%t=time, %%u=acquisition number, %%v=vendor, %%x=study ID; %%z=sequence name; default '%s')\n", kQstr, opts.filename); + printf(" -f : filename (%%a=antenna (coil) name, %%b=basename, %%c=comments, %%d=description, %%e=echo number, %%f=folder name, %%g=accession number, %%i=ID of patient, %%j=seriesInstanceUID, %%k=studyInstanceUID, %%m=manufacturer, %%n=name of patient, %%o=mediaObjectInstanceUID, %%p=protocol,%s %%r=instance number, %%s=series number, %%t=time, %%u=acquisition number, %%v=vendor, %%x=study ID; %%z=sequence name; default '%s')\n", kQstr, opts.filename); printf(" -g : generate defaults file (y/n/o/i [o=only: reset and write defaults; i=ignore: reset defaults], default n)\n"); printf(" -h : show help\n"); printf(" -i : ignore derived, localizer and 2D images (y/n, default n)\n"); @@ -131,6 +131,7 @@ void showHelp(const char * argv[], struct TDCMopts opts) { #endif printf(" --big-endian : byte order (y/n/o, default o) [y=big-end, n=little-end, o=optimal/native]\n"); printf(" --progress : report progress (y/n, default n)\n"); + printf(" --ignore_trigger_times : disregard values in 0018,1060 and 0020,9153\n"); printf(" --terse : omit filename post-fixes (can cause overwrites)\n"); printf(" --version : report version\n"); printf(" --xml : Slicer format features\n"); @@ -286,6 +287,9 @@ int main(int argc, const char * argv[]) opts.isSaveNativeEndian = false; printf("NIfTI data will be little-endian\n"); } + } else if ( ! strcmp(argv[i], "--ignore_trigger_times")) { + opts.isIgnoreTriggerTimes = true; + printf("ignore_trigger_times may have unintended consequences (issue 499)\n"); } else if ( ! strcmp(argv[i], "--terse")) { opts.isAddNamePostFixes = false; } else if ( ! strcmp(argv[i], "--version")) { @@ -400,9 +404,14 @@ int main(int argc, const char * argv[]) opts.isForceStackSameSeries = 1; if ((argv[i][0] == '2')) opts.isForceStackSameSeries = 2; - if ((argv[i][0] == 'o') || (argv[i][0] == 'O')) + if ((argv[i][0] == 'o') || (argv[i][0] == 'O')) { opts.isForceStackDCE = false; - + //printf("Advanced feature: '-m o' merges images despite varying series number\n"); + } + if ((argv[i][0] == '2')) { + opts.isIgnoreSeriesInstanceUID = true; + printf("Advanced feature: '-m 2' ignores Series Instance UID.\n"); + } } else if ((argv[i][1] == 'p') && ((i+1) < argc)) { i++; if (invalidParam(i, argv)) return 0; diff --git a/console/nii_dicom.cpp b/console/nii_dicom.cpp index ef71ae0e..59e37951 100644 --- a/console/nii_dicom.cpp +++ b/console/nii_dicom.cpp @@ -263,7 +263,7 @@ unsigned char * nii_loadImgCoreOpenJPEG(char* imgname, struct nifti_1_header hdr opj_destroy_codec(codec); return ret; } -#endif //if +#endif //myDisableOpenJPEG #ifndef M_PI #define M_PI 3.14159265358979323846 @@ -810,6 +810,7 @@ struct TDICOMdata clear_dicom_data() { d.is2DAcq = false; // d.isDerived = false; //0008,0008 = DERIVED,CSAPARALLEL,POSDISP d.isSegamiOasis = false; //these images do not store spatial coordinates + d.isBVecWorldCoordinates = false; //bvecs can be in image space (GE) or world coordinates (Siemens) d.isGrayscaleSoftcopyPresentationState = false; d.isRawDataStorage = false; d.isPartialFourier = false; @@ -1139,9 +1140,8 @@ int dcmStrManufacturer (const int lByteLength, unsigned char lBuffer[]) {//read // char cString[lByteLength + 1]; //#endif int ret = kMANUFACTURER_UNKNOWN; - cString[lByteLength] =0; + cString[lByteLength] = 0; memcpy(cString, (char*)&lBuffer[0], lByteLength); - //printMessage("MANU %s\n",cString); if ((toupper(cString[0])== 'S') && (toupper(cString[1])== 'I')) ret = kMANUFACTURER_SIEMENS; if ((toupper(cString[0])== 'G') && (toupper(cString[1])== 'E')) @@ -1159,6 +1159,8 @@ 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 (ret == kMANUFACTURER_UNKNOWN) + printWarning("Unknown manufacturer %s\n",cString); //#ifdef _MSC_VER free(cString); //#endif @@ -4052,12 +4054,22 @@ bool compareTDCMdim (const TDCMdim &dcm1, const TDCMdim &dcm2) { return false; } //compareTDCMdim() +bool compareTDCMdimRev (const TDCMdim &dcm1, const TDCMdim &dcm2) { + for (int i = 0; i < MAX_NUMBER_OF_DIMENSIONS; i++) { + if(dcm1.dimIdx[i] < dcm2.dimIdx[i]) + return true; + else if(dcm1.dimIdx[i] > dcm2.dimIdx[i]) + return false; + } + return false; +} //compareTDCMdimRev() + #else int compareTDCMdim(void const *item1, void const *item2) { struct TDCMdim const *dcm1 = (const struct TDCMdim *)item1; struct TDCMdim const *dcm2 = (const struct TDCMdim *)item2; - //for(int i=0; i < MAX_NUMBER_OF_DIMENSIONS; ++i){ + //for (int i = 0; i < MAX_NUMBER_OF_DIMENSIONS; i++) { for(int i=MAX_NUMBER_OF_DIMENSIONS-1; i >=0; i--){ if(dcm1->dimIdx[i] < dcm2->dimIdx[i]) @@ -4068,9 +4080,24 @@ int compareTDCMdim(void const *item1, void const *item2) { return 0; } //compareTDCMdim() +int compareTDCMdimRev(void const *item1, void const *item2) { + struct TDCMdim const *dcm1 = (const struct TDCMdim *)item1; + struct TDCMdim const *dcm2 = (const struct TDCMdim *)item2; + for (int i = 0; i < MAX_NUMBER_OF_DIMENSIONS; i++) { + if(dcm1->dimIdx[i] < dcm2->dimIdx[i]) + return -1; + else if(dcm1->dimIdx[i] > dcm2->dimIdx[i]) + return 1; + } + return 0; +} //compareTDCMdimRev() + #endif // USING_R -struct TDICOMdata readDICOMv(char * fname, int isVerbose, int compressFlag, struct TDTI4D *dti4D) { +struct TDICOMdata readDICOMx(char * fname, struct TDCMprefs* prefs, struct TDTI4D *dti4D) { +//struct TDICOMdata readDICOMv(char * fname, int isVerbose, int compressFlag, struct TDTI4D *dti4D) { + int isVerbose = prefs->isVerbose; + int compressFlag = prefs->compressFlag; struct TDICOMdata d = clear_dicom_data(); d.imageNum = 0; //not set strcpy(d.protocolName, ""); //erase dummy with empty @@ -4293,6 +4320,8 @@ const uint32_t kEffectiveTE = 0x0018+ (0x9082 << 16); #define kTriggerDelayTime 0x0020+uint32_t(0x9153<< 16 ) //FD #define kDimensionIndexValues 0x0020+uint32_t(0x9157<< 16 ) // UL n-dimensional index of frame. #define kInStackPositionNumber 0x0020+uint32_t(0x9057<< 16 ) // UL can help determine slices in volume + +#define kTemporalPositionIndex 0x0020+uint32_t(0x9128<< 16 ) // UL #define kDimensionIndexPointer 0x0020+uint32_t(0x9165<< 16 ) //Private Group 21 as Used by Siemens: #define kSequenceVariant21 0x0021+(0x105B<< 16 )//CS @@ -4321,6 +4350,7 @@ const uint32_t kEffectiveTE = 0x0018+ (0x9082 << 16); #define kFloatPixelPaddingValue 0x0028+(0x0122 << 16 ) // https://github.com/rordenlab/dcm2niix/issues/262 #define kIntercept 0x0028+(0x1052 << 16 ) #define kSlope 0x0028+(0x1053 << 16 ) +//#define kRescaleType 0x0028+(0x1053 << 16 ) //LO e.g. for Philips Fieldmap: [Hz] //#define kSpectroscopyDataPointColumns 0x0028+(0x9002 << 16 ) //IS #define kGeiisFlag 0x0029+(0x0010 << 16 ) //warn user if dreaded GEIIS was used to process image #define kCSAImageHeaderInfo 0x0029+(0x1010 << 16 ) @@ -4441,6 +4471,8 @@ uint32_t kSequenceDelimitationItemTag = 0xFFFE +(0xE0DD << 16 ); uint32_t dimensionIndexPointer[MAX_NUMBER_OF_DIMENSIONS]; size_t dimensionIndexPointerCounter = 0; int maxInStackPositionNumber = 0; + int temporalPositionIndex = 0; + int maxTemporalPositionIndex = 0; //int temporalPositionIdentifier = 0; int locationsInAcquisitionPhilips = 0; int imagesInAcquisition = 0; @@ -4496,6 +4528,7 @@ uint32_t kSequenceDelimitationItemTag = 0xFFFE +(0xE0DD << 16 ); bool isSwitchToBigEndian = false; bool isAtFirstPatientPosition = false; //for 3d and 4d files: flag is true for slices at same position as first slice bool isMosaic = false; + bool isGEfieldMap = false; //issue501 int patientPositionNum = 0; float B0Philips = -1.0; float vRLPhilips = 0.0; @@ -4604,7 +4637,7 @@ uint32_t kSequenceDelimitationItemTag = 0xFFFE +(0xE0DD << 16 ); is2005140FSQ = false; if (sqDepth < 0) sqDepth = 0; //should not happen, but protect for faulty anonymization //if we leave the folder MREchoSequence 0018,9114 - if (( nDimIndxVal > 0) && ((d.manufacturer == kMANUFACTURER_BRUKER) || (d.manufacturer == kMANUFACTURER_PHILIPS)) && (sqDepth00189114 >= sqDepth)) { + if (( nDimIndxVal > 0) && ((d.manufacturer == kMANUFACTURER_CANON) || (d.manufacturer == kMANUFACTURER_BRUKER) || (d.manufacturer == kMANUFACTURER_PHILIPS)) && (sqDepth00189114 >= sqDepth)) { sqDepth00189114 = -1; //triggered //printf("slice %d---> 0020,9157 = %d %d %d\n", inStackPositionNumber, d.dimensionIndexValues[0], d.dimensionIndexValues[1], d.dimensionIndexValues[2]); if (inStackPositionNumber > 0) { @@ -4639,7 +4672,8 @@ uint32_t kSequenceDelimitationItemTag = 0xFFFE +(0xE0DD << 16 ); // Bruker Enhanced MR IOD: reorder dimensions to ensure InStackPositionNumber corresponds to the first one // This will ensure correct ordering of slices in 4D datasets - if (d.manufacturer == kMANUFACTURER_BRUKER) { + /* + if (d.manufacturer == kMANUFACTURER_BRUKER) { for(size_t i = 1; i < dimensionIndexPointerCounter; i++){ if (dimensionIndexPointer[i] == kInStackPositionNumber){ //swap with first @@ -4647,8 +4681,9 @@ uint32_t kSequenceDelimitationItemTag = 0xFFFE +(0xE0DD << 16 ); dimensionIndexOrder[0] = i; } } - } + }*/ //Canon and Bruker reverse dimensionIndexItem order relative to Philips: new versions introduce compareTDCMdimRev int ndim = nDimIndxVal; + //printf("%d: %d %d %d %d\n", ndim, d.dimensionIndexValues[0], d.dimensionIndexValues[1], d.dimensionIndexValues[2], d.dimensionIndexValues[3]); for (int i = 0; i < ndim; i++) dcmDim[numDimensionIndexValues].dimIdx[i] = d.dimensionIndexValues[dimensionIndexOrder[i]]; dcmDim[numDimensionIndexValues].TE = TE; @@ -4935,7 +4970,7 @@ uint32_t kSequenceDelimitationItemTag = 0xFFFE +(0xE0DD << 16 ); privateCreatorRemaps[nRemaps] = privateCreatorRemap; //printf("new remapping %04x,%04x -> %04x,%04x\n", privateCreatorMask & 65535, privateCreatorMask >> 16, privateCreatorRemap & 65535, privateCreatorRemap >> 16); if (isVerbose > 1) - printf("new remapping (%d) %04x,%02xxy -> %04x,%02xxy\n", nRemaps, privateCreatorMask & 65535, privateCreatorMask >> 24, privateCreatorRemap & 65535, privateCreatorRemap >> 24); + printMessage("new remapping (%d) %04x,%02xxy -> %04x,%02xxy\n", nRemaps, privateCreatorMask & 65535, privateCreatorMask >> 24, privateCreatorRemap & 65535, privateCreatorRemap >> 24); nRemaps += 1; //for (int i = 0; i < nRemaps; i++) // printf(" %d = %04x,%02xxy -> %04x,%02xxy\n", i, privateCreatorMasks[i] & 65535, privateCreatorMasks[i] >> 24, privateCreatorRemaps[i] & 65535, privateCreatorRemaps[i] >> 24); @@ -4950,13 +4985,13 @@ uint32_t kSequenceDelimitationItemTag = 0xFFFE +(0xE0DD << 16 ); remappedGroupElement = privateCreatorRemaps[i] + (groupElement & 0x00FF0000); if (remappedGroupElement == 0) goto skipRemap; if (isVerbose > 1) - printf("remapping %04x,%04x -> %04x,%04x\n", groupElement & 65535, groupElement >> 16, remappedGroupElement & 65535, remappedGroupElement >> 16); + printMessage("remapping %04x,%04x -> %04x,%04x\n", groupElement & 65535, groupElement >> 16, remappedGroupElement & 65535, remappedGroupElement >> 16); groupElement = remappedGroupElement; } skipRemap: #endif // salvageAgfa if ((lLength % 2) != 0) { //https://www.nitrc.org/forum/forum.php?thread_id=11827&forum_id=4703 - printf("Illegal DICOM tag %04x,%04x (odd element length %d): %s\n", groupElement & 65535,groupElement>>16, lLength, fname); + printMessage("Illegal DICOM tag %04x,%04x (odd element length %d): %s\n", groupElement & 65535,groupElement>>16, lLength, fname); //proper to return here, but we can carry on as a hail mary // d.isValid = false; //return d; @@ -5185,7 +5220,8 @@ uint32_t kSequenceDelimitationItemTag = 0xFFFE +(0xE0DD << 16 ); d.modality = kMODALITY_US; break; case kManufacturer: - d.manufacturer = dcmStrManufacturer (lLength, &buffer[lPos]); + if (d.manufacturer == kMANUFACTURER_UNKNOWN) + d.manufacturer = dcmStrManufacturer (lLength, &buffer[lPos]); volDiffusion.manufacturer = d.manufacturer; break; case kInstitutionName: @@ -5510,6 +5546,8 @@ uint32_t kSequenceDelimitationItemTag = 0xFFFE +(0xE0DD << 16 ); if (strcmp(epiStr, "EPI2") == 0){ d.internalepiVersionGE = 2; //-1 = not epi, 1 = EPI, 2 = EPI2 } + if ((strcmp(epiStr, "EFGRE3D") == 0) || (strcmp(epiStr, "B0map") == 0)) + isGEfieldMap = true; //issue501 break; } case kBandwidthPerPixelPhaseEncode: @@ -5591,12 +5629,16 @@ uint32_t kSequenceDelimitationItemTag = 0xFFFE +(0xE0DD << 16 ); if (d.imageNum < 1) d.imageNum = dcmStrInt(lLength, &buffer[lPos]); //Philips renames each image again in 2001,9000, which can lead to duplicates break; case kInStackPositionNumber: - if ((d.manufacturer != kMANUFACTURER_HITACHI) && (d.manufacturer != kMANUFACTURER_UNKNOWN) && (d.manufacturer != kMANUFACTURER_PHILIPS) && (d.manufacturer != kMANUFACTURER_BRUKER)) break; + if ((d.manufacturer != kMANUFACTURER_CANON) && (d.manufacturer != kMANUFACTURER_HITACHI) && (d.manufacturer != kMANUFACTURER_UNKNOWN) && (d.manufacturer != kMANUFACTURER_PHILIPS) && (d.manufacturer != kMANUFACTURER_BRUKER)) break; inStackPositionNumber = dcmInt(4,&buffer[lPos],d.isLittleEndian); //if (inStackPositionNumber == 1) numInStackPositionNumber1 ++; //printf("<%d>\n",inStackPositionNumber); if (inStackPositionNumber > maxInStackPositionNumber) maxInStackPositionNumber = inStackPositionNumber; break; + case kTemporalPositionIndex: + temporalPositionIndex = dcmInt(4,&buffer[lPos],d.isLittleEndian); + if (temporalPositionIndex > maxTemporalPositionIndex) maxTemporalPositionIndex = temporalPositionIndex; + break; case kDimensionIndexPointer: dimensionIndexPointer[dimensionIndexPointerCounter++] = dcmAttributeTag(&buffer[lPos],d.isLittleEndian); break; @@ -5606,6 +5648,7 @@ uint32_t kSequenceDelimitationItemTag = 0xFFFE +(0xE0DD << 16 ); sqDepth00189114 = sqDepth - 1; break; case kTriggerDelayTime: { //0x0020+uint32_t(0x9153<< 16 ) //FD + if (prefs->isIgnoreTriggerTimes) break;//issue499 if (d.manufacturer != kMANUFACTURER_PHILIPS) break; //if (isVerbose < 2) break; double trigger = dcmFloatDouble(lLength, &buffer[lPos],d.isLittleEndian); @@ -5615,13 +5658,13 @@ uint32_t kSequenceDelimitationItemTag = 0xFFFE +(0xE0DD << 16 ); case kDimensionIndexValues: { // kImageNum is not enough for 4D series from Philips 5.*. if (lLength < 4) break; nDimIndxVal = lLength / 4; - if(nDimIndxVal > MAX_NUMBER_OF_DIMENSIONS){ + if(nDimIndxVal > MAX_NUMBER_OF_DIMENSIONS){ printError("%d is too many dimensions. Only up to %d are supported\n", nDimIndxVal, MAX_NUMBER_OF_DIMENSIONS); nDimIndxVal = MAX_NUMBER_OF_DIMENSIONS; // Truncate } dcmMultiLongs(4 * nDimIndxVal, &buffer[lPos], nDimIndxVal, d.dimensionIndexValues, d.isLittleEndian); - break; } + break; } case kPhotometricInterpretation: { char interp[kDICOMStr]; dcmStr(lLength, &buffer[lPos], interp); @@ -5804,6 +5847,7 @@ uint32_t kSequenceDelimitationItemTag = 0xFFFE +(0xE0DD << 16 ); d.imagingFrequency = dcmStrFloat(lLength, &buffer[lPos]); break; case kTriggerTime: { + if (prefs->isIgnoreTriggerTimes) break;//issue499 //untested method to detect slice timing for GE PSD “epi” with multiphase option // will not work for current PSD “epiRT” (BrainWave RT, fMRI/DTI package provided by Medical Numerics) if ((d.manufacturer != kMANUFACTURER_GE) && (d.manufacturer != kMANUFACTURER_PHILIPS)) break; //issue384 @@ -6199,6 +6243,7 @@ uint32_t kSequenceDelimitationItemTag = 0xFFFE +(0xE0DD << 16 ); //d.CSA.dtiV[3] = v[2]; //printMessage("><>< 0018,9089: DWI bxyz %g %g %g %g\n", d.CSA.dtiV[0], d.CSA.dtiV[1], d.CSA.dtiV[2], d.CSA.dtiV[3]); 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); } break; @@ -6798,6 +6843,12 @@ uint32_t kSequenceDelimitationItemTag = 0xFFFE +(0xE0DD << 16 ); //printMessage("Issue 373: Check for ZIP2 Factor: %d SliceThickness+SliceGap: %f, SpacingBetweenSlices: %f \n", zipFactor, d.xyzMM[3], d.zSpacing); locationsInAcquisitionGE *= zipFactor; // Multiply number of slices by ZIP factor. Do this prior to checking for conflict below (?). } + if (isGEfieldMap) { //issue501 : to do check zip factor + //Volume 1) derived phase field map [Hz] and 2) magnitude volume. + d.isDerived = (d.imageNum <= locationsInAcquisitionGE); //first volume + d.isRealIsPhaseMapHz = d.isDerived; + d.isHasReal = d.isDerived; + } /* SAH.end */ if (locationsInAcquisitionGE < d.locationsInAcquisition) { d.locationsInAcquisitionConflict = d.locationsInAcquisition; @@ -6989,9 +7040,10 @@ if (d.isHasPhase) // printWarning("3D EPI with FrameAcquisitionDuration = %gs volumes = %d (see issue 369)\n", frameAcquisitionDuration/1000.0, d.xyzDim[4]); if (numDimensionIndexValues > 1) strcpy(d.imageType, imageType1st); //for multi-frame datasets, return name of book, not name of last chapter - if ((numDimensionIndexValues > 1) && (numDimensionIndexValues == numberOfFrames)) { + if ((numDimensionIndexValues > 1) && (numDimensionIndexValues == numberOfFrames)) { //Philips enhanced datasets can have custom slice orders and pack images with different TE, Phase/Magnitude/Etc. - if (isVerbose > 1) { // + int maxVariableItem = 0; + if (true) { // int mn[MAX_NUMBER_OF_DIMENSIONS]; int mx[MAX_NUMBER_OF_DIMENSIONS]; for (int j = 0; j < MAX_NUMBER_OF_DIMENSIONS; j++) { @@ -7001,20 +7053,35 @@ if (d.isHasPhase) if (mx[j] < dcmDim[i].dimIdx[j]) mx[j] = dcmDim[i].dimIdx[j]; if (mn[j] > dcmDim[i].dimIdx[j]) mn[j] = dcmDim[i].dimIdx[j]; } + if (mx[j] != mn[j]) maxVariableItem = j; } - printMessage(" DimensionIndexValues (0020,9157), dimensions with variability:\n"); - for (int i = 0; i < MAX_NUMBER_OF_DIMENSIONS; i++) - if (mn[i] != mx[i]) - printMessage(" Dimension %d Range: %d..%d\n", i, mn[i], mx[i]); + if (isVerbose > 1) { + printMessage(" DimensionIndexValues (0020,9157), dimensions with variability:\n"); + for (int i = 0; i < MAX_NUMBER_OF_DIMENSIONS; i++) + if (mn[i] != mx[i]) + printMessage(" Dimension %d Range: %d..%d\n", i, mn[i], mx[i]); + } } //verbose > 1 + //see http://dicom.nema.org/medical/Dicom/2018d/output/chtml/part03/sect_C.8.24.3.3.html + //Philips puts spatial position as lower item than temporal position, the reverse is true for Bruker and Canon + int stackPositionItem = 0; + if (dimensionIndexPointerCounter > 0) + for(size_t i = 0; i < dimensionIndexPointerCounter; i++) + if (dimensionIndexPointer[i] == kInStackPositionNumber) stackPositionItem = i; //sort dimensions #ifdef USING_R - std::sort(dcmDim.begin(), dcmDim.begin() + numberOfFrames, compareTDCMdim); + if (stackPositionItem < maxVariableItem) + std::sort(dcmDim.begin(), dcmDim.begin() + numberOfFrames, compareTDCMdim); + else + std::sort(dcmDim.begin(), dcmDim.begin() + numberOfFrames, compareTDCMdimRev); #else - qsort(dcmDim, numberOfFrames, sizeof(struct TDCMdim), compareTDCMdim); + if (stackPositionItem < maxVariableItem) + qsort(dcmDim, numberOfFrames, sizeof(struct TDCMdim), compareTDCMdim); + else + qsort(dcmDim, numberOfFrames, sizeof(struct TDCMdim), compareTDCMdimRev); #endif //for (int i = 0; i < numberOfFrames; i++) - // printf("diskPos= %d dimIdx= %d %d %d %d TE= %g\n", i, dcmDim[i].diskPos, dcmDim[i].dimIdx[1], dcmDim[i].dimIdx[2], dcmDim[i].dimIdx[3], dti4D->TE[i]); + // printf("i %d diskPos= %d dimIdx= %d %d %d %d TE= %g\n", i, dcmDim[i].diskPos, dcmDim[i].dimIdx[0], dcmDim[i].dimIdx[1], dcmDim[i].dimIdx[2], dcmDim[i].dimIdx[3], dti4D->TE[i]); for (int i = 0; i < numberOfFrames; i++) { dti4D->sliceOrder[i] = dcmDim[i].diskPos; dti4D->intenScale[i] = dcmDim[i].intenScale; @@ -7264,6 +7331,22 @@ if (d.isHasPhase) return d; } // readDICOM() +void setDefaultPrefs (struct TDCMprefs *prefs) { + prefs->isVerbose = false; + prefs->compressFlag = kCompressSupport; + prefs->isIgnoreTriggerTimes = false; +} + +struct TDICOMdata readDICOMv(char * fname, int isVerbose, int compressFlag, struct TDTI4D *dti4D) { + struct TDCMprefs prefs; + setDefaultPrefs(&prefs); + prefs.isVerbose = isVerbose; + prefs.compressFlag = compressFlag; + TDICOMdata ret = readDICOMx(fname, &prefs, dti4D); + return ret; +} + + struct TDICOMdata readDICOM(char * fname) { struct TDTI4D *dti4D = (struct TDTI4D *)malloc(sizeof(struct TDTI4D)); //unused TDICOMdata ret = readDICOMv(fname, false, kCompressSupport, dti4D); diff --git a/console/nii_dicom.h b/console/nii_dicom.h index cc6e51e8..78962342 100644 --- a/console/nii_dicom.h +++ b/console/nii_dicom.h @@ -50,7 +50,7 @@ extern "C" { #define kCPUsuf " " //unknown CPU #endif -#define kDCMdate "v1.0.20210129" +#define kDCMdate "v1.0.20210410" #define kDCMvers kDCMdate " " kJP2suf kLSsuf kCCsuf kCPUsuf static const int kMaxEPI3D = 1024; //maximum number of EPI images in Siemens Mosaic @@ -192,15 +192,21 @@ static const uint8_t MAX_NUMBER_OF_DIMENSIONS = 8; char institutionAddress[kDICOMStrLarge], imageComments[kDICOMStrLarge]; uint32_t dimensionIndexValues[MAX_NUMBER_OF_DIMENSIONS]; struct TCSAdata CSA; - bool isRealIsPhaseMapHz, isPrivateCreatorRemap, isHasOverlay, isEPI, isIR, isPartialFourier, isDiffusion, isVectorFromBMatrix, isRawDataStorage, isGrayscaleSoftcopyPresentationState, isStackableSeries, isCoilVaries, isNonParallelSlices, isSegamiOasis, isXA10A, isScaleOrTEVaries, isScaleVariesEnh, isDerived, isXRay, isMultiEcho, isValid, is3DAcq, is2DAcq, isExplicitVR, isLittleEndian, isPlanarRGB, isSigned, isHasPhase, isHasImaginary, isHasReal, isHasMagnitude,isHasMixed, isFloat, isResampled, isLocalizer; + 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; char phaseEncodingRC, patientSex; }; + struct TDCMprefs { + int isVerbose, compressFlag, isIgnoreTriggerTimes; + }; size_t nii_ImgBytes(struct nifti_1_header hdr); + void setDefaultPrefs (struct TDCMprefs *prefs); int isSameFloatGE (float a, float b); void getFileNameX( char *pathParent, const char *path, int maxLen); struct TDICOMdata readDICOMv(char * fname, int isVerbose, int compressFlag, struct TDTI4D *dti4D); - struct TDICOMdata readDICOM(char * fname); + struct TDICOMdata readDICOMx(char * fname, struct TDCMprefs* prefs, struct TDTI4D *dti4D); + + struct TDICOMdata readDICOM(char * fname); struct TDICOMdata clear_dicom_data(void); struct TDICOMdata nii_readParRec (char * parname, int isVerbose, struct TDTI4D *dti4D, bool isReadPhase); unsigned char * nii_flipY(unsigned char* bImg, struct nifti_1_header *h); diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index 524e5ca1..954d1360 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -69,6 +69,9 @@ #undef isnan #define isnan ISNAN + +#undef isfinite +#define isfinite R_FINITE #endif #define newTilt @@ -176,6 +179,13 @@ bool is_exe(const char* path) { //requires #include }// is_dir() #endif +void opts2Prefs (struct TDCMopts* opts, struct TDCMprefs *prefs) { + setDefaultPrefs(prefs); + prefs->isVerbose = opts->isVerbose; + prefs->compressFlag = opts->compressFlag; + prefs->isIgnoreTriggerTimes = opts->isIgnoreTriggerTimes; +} + void geCorrectBvecs(struct TDICOMdata *d, int sliceDir, struct TDTI *vx, int isVerbose){ //0018,1312 phase encoding is either in row or column direction //0043,1039 (or 0043,a039). b value (as the first number in the string). @@ -188,6 +198,7 @@ void geCorrectBvecs(struct TDICOMdata *d, int sliceDir, struct TDTI *vx, int isV //COL then if swap the x and y value and reverse the sign on the z value. //If the phase encoding is not COL, then just reverse the sign on the x value. if ((d->manufacturer != kMANUFACTURER_GE) && (d->manufacturer != kMANUFACTURER_CANON)) return; + if (d->isBVecWorldCoordinates) return; //Canon classic DICOMs use image space, enhanced use world space! if ((!d->isEPI) && (d->CSA.numDti == 1)) d->CSA.numDti = 0; //issue449 if (d->CSA.numDti < 1) return; if ((toupper(d->patientOrient[0])== 'H') && (toupper(d->patientOrient[1])== 'F') && (toupper(d->patientOrient[2])== 'S')) @@ -204,7 +215,7 @@ void geCorrectBvecs(struct TDICOMdata *d, int sliceDir, struct TDTI *vx, int isV return; } if (abs(sliceDir) != 3) - printWarning("GE DTI only tested for axial acquisitions (solution: use Xiangrui Li's dicm2nii)\n"); + printWarning("Limited validation for non-Axial DTI: confirm gradient vector transformation.\n"); //GE vectors from Xiangrui Li' dicm2nii, validated with datasets from https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Diffusion_Tensor_Imaging ivec3 flp; if (abs(sliceDir) == 1) @@ -291,16 +302,18 @@ void siemensPhilipsCorrectBvecs(struct TDICOMdata *d, int sliceDir, struct TDTI //convert DTI vectors from scanner coordinates to image frame of reference //Uses 6 orient values from ImageOrientationPatient (0020,0037) // requires PatientPosition 0018,5100 is HFS (head first supine) - if ((d->manufacturer != kMANUFACTURER_BRUKER) && (d->manufacturer != kMANUFACTURER_TOSHIBA) && (d->manufacturer != kMANUFACTURER_HITACHI) && (d->manufacturer != kMANUFACTURER_UIH) && (d->manufacturer != kMANUFACTURER_SIEMENS) && (d->manufacturer != kMANUFACTURER_PHILIPS)) return; + if ((!d->isBVecWorldCoordinates) && (d->manufacturer != kMANUFACTURER_BRUKER) && (d->manufacturer != kMANUFACTURER_TOSHIBA) && (d->manufacturer != kMANUFACTURER_HITACHI) && (d->manufacturer != kMANUFACTURER_UIH) && (d->manufacturer != kMANUFACTURER_SIEMENS) && (d->manufacturer != kMANUFACTURER_PHILIPS)) return; if (d->CSA.numDti < 1) return; - if (d->manufacturer == kMANUFACTURER_UIH) { + if (d->manufacturer == kMANUFACTURER_UIH) { for (int i = 0; i < d->CSA.numDti; i++) { vx[i].V[2] = -vx[i].V[2]; for (int v= 0; v < 4; v++) if (vx[i].V[v] == -0.0f) vx[i].V[v] = 0.0f; //remove sign from values that are virtually zero } +#ifndef USING_R for (int i = 0; i < 3; i++) printf("%g = %g %g %g\n", vx[i].V[0], vx[i].V[1], vx[i].V[2], vx[i].V[3]); +#endif return; } //https://github.com/rordenlab/dcm2niix/issues/225 if ((toupper(d->patientOrient[0])== 'H') && (toupper(d->patientOrient[1])== 'F') && (toupper(d->patientOrient[2])== 'S')) @@ -377,9 +390,7 @@ void nii_saveText(char pathoutname[], struct TDICOMdata d, struct TDCMopts opts, fclose(fp); }// nii_saveText() -#ifndef USING_R #define myReadAsciiCsa -#endif #ifdef myReadAsciiCsa //read from the ASCII portion of the Siemens CSA series header @@ -812,12 +823,12 @@ int geProtocolBlock(const char * filename, int geOffset, int geLength, int isV bool isFCOMMENT = ((flags & 0x10) == 0x10); uint32_t hdrSz = 10; if (isFNAME) {//skip null-terminated string FNAME - for (hdrSz = hdrSz; hdrSz < cmpSz; hdrSz++) + for (; hdrSz < cmpSz; hdrSz++) if (pCmp[hdrSz] == 0) break; hdrSz++; } if (isFCOMMENT) {//skip null-terminated string COMMENT - for (hdrSz = hdrSz; hdrSz < cmpSz; hdrSz++) + for (; hdrSz < cmpSz; hdrSz++) if (pCmp[hdrSz] == 0) break; hdrSz++; } @@ -1190,6 +1201,9 @@ tse3d: T2*/ fprintf(fp, "\t\"PhilipsScaleSlope\": %g,\n", d.intenScalePhilips ); fprintf(fp, "\t\"UsePhilipsFloatNotDisplayScaling\": %d,\n", opts.isPhilipsFloatNotDisplayScaling); } + //https://bids-specification--622.org.readthedocs.build/en/622/04-modality-specific-files/01-magnetic-resonance-imaging-data.html#case-3-direct-field-mapping + if ((d.isRealIsPhaseMapHz) && (d.isHasReal)) + fprintf(fp, "\t\"Units\": \"Hz\",\n"); // //PET ISOTOPE MODULE ATTRIBUTES json_Str(fp, "\t\"Radiopharmaceutical\": \"%s\",\n", d.radiopharmaceutical); json_Float(fp, "\t\"RadionuclidePositronFraction\": %g,\n", d.radionuclidePositronFraction ); @@ -1375,7 +1389,7 @@ tse3d: T2*/ } //for k */ for (int k = 3; k < 11; k++) { //vessel locations char newstr[256]; - sprintf(newstr, "\t\"sWipMemBlock.AdFree%d\": %%g,\n", k); + sprintf(newstr, "\t\"sWipMemBlockAdFree%d\": %%g,\n", k); //issue483: sWipMemBlock.AdFree -> sWipMemBlockAdFree json_FloatNotNan(fp, newstr, csaAscii.adFree[k]); } } @@ -1545,9 +1559,10 @@ tse3d: T2*/ // effectiveEchoSpacing = d.CSA.sliceMeasurementDuration / (reconMatrixPE * 1000.0); if ((reconMatrixPE > 0) && (bandwidthPerPixelPhaseEncode > 0.0)) effectiveEchoSpacing = 1.0 / (bandwidthPerPixelPhaseEncode * reconMatrixPE); - if ((effectiveEchoSpacing == 0.0) && (d.fieldStrength > 0) && (d.waterFatShift != 0.0) && (d.echoTrainLength > 0) && (reconMatrixPE > 1)) { - json_Float(fp, "\t\"WaterFatShift\": %g,\n", d.waterFatShift); - //https://github.com/rordenlab/dcm2niix/issues/377 + json_Float(fp, "\t\"WaterFatShift\": %g,\n", d.waterFatShift); + if ((effectiveEchoSpacing == 0.0) && (d.imagingFrequency > 0.0) && (d.waterFatShift != 0.0) && (d.echoTrainLength > 0) && (reconMatrixPE > 1)) { + //in theory we could use either fieldStrength or imagingFrequency, but the former is typically provided with low precision + //https://github.com/rordenlab/dcm2niix/issues/377 // EchoSpacing 1/BW/EPI_factor https://www.jiscmail.ac.uk/cgi-bin/webadmin?A2=ind1308&L=FSL&D=0&P=113520 // this formula from https://support.brainvoyager.com/brainvoyager/functional-analysis-preparation/29-pre-processing/78-epi-distortion-correction-echo-spacing-and-bandwidth // https://neurostars.org/t/consolidating-epi-echo-spacing-and-readout-time-for-philips-scanner/4406 @@ -1561,22 +1576,21 @@ tse3d: T2*/ ReconMatrixPE = 0028,0010 or 0028,0011 depending on 0018,1312 */ - float actualEchoSpacing = d.waterFatShift / (d.imagingFrequency * 3.4 * (d.echoTrainLength + 1)); + float actualEchoSpacing = d.waterFatShift / (d.imagingFrequency * 3.4 * (d.echoTrainLength + 1)); float totalReadoutTime = actualEchoSpacing * d.echoTrainLength; float effectiveEchoSpacingPhil = totalReadoutTime / (reconMatrixPE - 1); json_Float(fp, "\t\"EstimatedEffectiveEchoSpacing\": %g,\n", effectiveEchoSpacingPhil); - fprintf(fp, "\t\"EstimatedTotalReadoutTime\": %g,\n", totalReadoutTime); + + fprintf(fp, "\t\"EstimatedTotalReadoutTime\": %g,\n", totalReadoutTime); } if (d.effectiveEchoSpacingGE > 0.0) { //TotalReadoutTime = [ ceil (PE_AcquisitionMatrix / Asset_R_factor) - 1] * ESP float roundFactor = 2.0; if (d.isPartialFourier) roundFactor = 4.0; float totalReadoutTime = ((ceil (1/roundFactor * d.phaseEncodingLines / d.accelFactPE) * roundFactor) - 1.0) * d.effectiveEchoSpacingGE * 0.000001; - printf("ASSET= %g PE_AcquisitionMatrix= %d ESP= %d TotalReadoutTime= %g\n", d.accelFactPE, d.phaseEncodingLines, d.effectiveEchoSpacingGE, totalReadoutTime); - json_Float(fp, "\t\"TotalReadoutTime\": %g,\n", totalReadoutTime); - float effectiveEchoSpacingGE = totalReadoutTime / (reconMatrixPE - 1); - json_Float(fp, "\t\"EstimatedEffectiveEchoSpacing\": %g,\n", effectiveEchoSpacingGE); - effectiveEchoSpacing = 0.0; + //printf("ASSET= %g PE_AcquisitionMatrix= %d ESP= %d TotalReadoutTime= %g\n", d.accelFactPE, d.phaseEncodingLines, d.effectiveEchoSpacingGE, totalReadoutTime); + //json_Float(fp, "\t\"TotalReadoutTime\": %g,\n", totalReadoutTime); + effectiveEchoSpacing = totalReadoutTime / (reconMatrixPE - 1); } json_Float(fp, "\t\"EffectiveEchoSpacing\": %g,\n", effectiveEchoSpacing); // Calculate true echo spacing (should match what Siemens reports on the console) @@ -1674,6 +1688,8 @@ tse3d: T2*/ fclose(fp); }// nii_SaveBIDSX() +#ifndef USING_R + void nii_SaveBIDS(char pathoutname[], struct TDICOMdata d, struct TDCMopts opts, struct nifti_1_header *h, const char * filename) { struct TDTI4D *dti4D; dti4D->sliceOrder[0] = -1; @@ -1685,6 +1701,8 @@ dti4D->repetitionTimeInversion = 0.0; nii_SaveBIDSX(pathoutname, d, opts, h, filename, dti4D); }// nii_SaveBIDSX() +#endif + bool isADCnotDTI(TDTI bvec) { //returns true if bval!=0 but all bvecs == 0 (Philips code for derived ADC image) return ((!isSameFloat(bvec.V[0],0.0f)) && //not a B-0 image ((isSameFloat(bvec.V[1],0.0f)) && (isSameFloat(bvec.V[2],0.0f)) && (isSameFloat(bvec.V[3],0.0f)) ) ); @@ -2667,8 +2685,8 @@ int nii_createFilename(struct TDICOMdata dcm, char * niiFilename, struct TDCMopt strcat (outname,newstr); isEchoReported = true; } - if ((dcm.isNonParallelSlices) && (!isImageNumReported)) { - sprintf(newstr, "_i%05d", dcm.imageNum); + if ((dcm.isNonParallelSlices) && (!isImageNumReported)) { + sprintf(newstr, "_i%05d", dcm.imageNum); strcat (outname,newstr); } /*if (dcm.maxGradDynVol > 0) { //Philips segmented @@ -2711,7 +2729,7 @@ int nii_createFilename(struct TDICOMdata dcm, char * niiFilename, struct TDCMopt #endif #if defined(_WIN64) || defined(_WIN32) || defined(kMASK_WINDOWS_SPECIAL_CHARACTERS)//https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names for (size_t pos = 0; pos') || (outname[pos] == ':') + if ((outname[pos] == '\\') || (outname[pos] == '/') || (outname[pos] == ' ') || (outname[pos] == '<') || (outname[pos] == '>') || (outname[pos] == ':') || (outname[pos] == ';') || (outname[pos] == '"') // || (outname[pos] == '/') || (outname[pos] == '\\') //|| (outname[pos] == '^') issue398 || (outname[pos] == '*') || (outname[pos] == '|') || (outname[pos] == '?')) @@ -2753,7 +2771,7 @@ int nii_createFilename(struct TDICOMdata dcm, char * niiFilename, struct TDCMopt #if defined(USING_R) && (defined(_WIN64) || defined(_WIN32)) // R also uses forward slash on Windows, so allow it here if (!sep) - sep = strchr(outname, kForeignPathSeparator); + sep = strchr(outname, '/'); #endif if (sep) { char newdir[2048] = {""}; @@ -2992,37 +3010,23 @@ void nii_saveAttributes (struct TDICOMdata &data, struct nifti_1_header &header, case kMANUFACTURER_HITACHI: images->addAttribute("manufacturer", "Hitachi"); break; case kMANUFACTURER_CANON: images->addAttribute("manufacturer", "Canon"); break; } - if (strlen(data.manufacturersModelName) > 0) - images->addAttribute("scannerModelName", data.manufacturersModelName); - if (strlen(data.imageType) > 0) - images->addAttribute("imageType", data.imageType); + images->addAttribute("scannerModelName", data.manufacturersModelName); + images->addAttribute("imageType", data.imageType); if (data.seriesNum > 0) images->addAttribute("seriesNumber", int(data.seriesNum)); - if (strlen(data.seriesDescription) > 0) - images->addAttribute("seriesDescription", data.seriesDescription); - if (strlen(data.sequenceName) > 0) - images->addAttribute("sequenceName", data.sequenceName); - if (strlen(data.protocolName) > 0) - images->addAttribute("protocolName", data.protocolName); - if (strlen(data.studyDate) >= 8 && strcmp(data.studyDate,"00000000") != 0) - images->addDateAttribute("studyDate", data.studyDate); - if (strlen(data.studyTime) > 0 && strncmp(data.studyTime,"000000",6) != 0) - images->addAttribute("studyTime", data.studyTime); - if (data.fieldStrength > 0.0) - images->addAttribute("fieldStrength", data.fieldStrength); - if (data.flipAngle > 0.0) - images->addAttribute("flipAngle", data.flipAngle); - if (data.TE > 0.0) - images->addAttribute("echoTime", data.TE); - if (data.TR > 0.0) - images->addAttribute("repetitionTime", data.TR); - if (data.TI > 0.0) - images->addAttribute("inversionTime", data.TI); + images->addAttribute("seriesDescription", data.seriesDescription); + images->addAttribute("sequenceName", data.sequenceName); + images->addAttribute("protocolName", data.protocolName); + images->addDateAttribute("studyDate", data.studyDate); + images->addTimeAttribute("studyTime", data.studyTime); + images->addAttribute("fieldStrength", data.fieldStrength); + images->addAttribute("flipAngle", data.flipAngle); + images->addAttribute("echoTime", data.TE); + images->addAttribute("repetitionTime", data.TR); + images->addAttribute("inversionTime", data.TI); if (!data.isXRay) { - if (data.zThick > 0.0) - images->addAttribute("sliceThickness", data.zThick); - if (data.zSpacing > 0.0) - images->addAttribute("sliceSpacing", data.zSpacing); + images->addAttribute("sliceThickness", data.zThick); + images->addAttribute("sliceSpacing", data.zSpacing); } if (data.CSA.multiBandFactor > 1) images->addAttribute("multibandFactor", data.CSA.multiBandFactor); @@ -3050,18 +3054,21 @@ void nii_saveAttributes (struct TDICOMdata &data, struct nifti_1_header &header, double effectiveEchoSpacing = 0.0; if ((reconMatrixPE > 0) && (bandwidthPerPixelPhaseEncode > 0.0)) effectiveEchoSpacing = 1.0 / (bandwidthPerPixelPhaseEncode * reconMatrixPE); - if (data.effectiveEchoSpacingGE > 0.0) - effectiveEchoSpacing = data.effectiveEchoSpacingGE / 1000000.0; - - if (effectiveEchoSpacing > 0.0) - images->addAttribute("effectiveEchoSpacing", effectiveEchoSpacing); - if ((reconMatrixPE > 0) && (effectiveEchoSpacing > 0.0)) + if (data.effectiveEchoSpacingGE > 0.0) { + double roundFactor = data.isPartialFourier ? 4.0 : 2.0; + double totalReadoutTime = ((ceil(1.0/roundFactor * data.phaseEncodingLines / data.accelFactPE) * roundFactor) - 1.0) * data.effectiveEchoSpacingGE * 0.000001; + effectiveEchoSpacing = totalReadoutTime / (reconMatrixPE - 1); + } + + images->addAttribute("effectiveEchoSpacing", effectiveEchoSpacing); + if (data.manufacturer == kMANUFACTURER_UIH) + images->addAttribute("effectiveReadoutTime", data.acquisitionDuration / 1000.0); + else if ((reconMatrixPE > 0) && (effectiveEchoSpacing > 0.0)) images->addAttribute("effectiveReadoutTime", effectiveEchoSpacing * (reconMatrixPE - 1.0)); - if (data.pixelBandwidth > 0.0) - images->addAttribute("pixelBandwidth", data.pixelBandwidth); + images->addAttribute("pixelBandwidth", data.pixelBandwidth); if ((data.manufacturer == kMANUFACTURER_SIEMENS) && (data.dwellTime > 0)) images->addAttribute("dwellTime", data.dwellTime * 1e-9); - + // Phase encoding polarity // We only save these attributes if both direction and polarity are known bool isSkipPhaseEncodingAxis = data.is3DAcq; @@ -3091,22 +3098,17 @@ void nii_saveAttributes (struct TDICOMdata &data, struct nifti_1_header &header, images->addAttribute("sliceTiming", sliceTimes); } - if (strlen(data.patientID) > 0) - images->addAttribute("patientIdentifier", data.patientID); - if (strlen(data.patientName) > 0) - images->addAttribute("patientName", data.patientName); - if (strlen(data.patientBirthDate) >= 8 && strcmp(data.patientBirthDate,"00000000") != 0) - images->addDateAttribute("patientBirthDate", data.patientBirthDate); + images->addAttribute("patientIdentifier", data.patientID); + images->addAttribute("patientName", data.patientName); + images->addDateAttribute("patientBirthDate", data.patientBirthDate); if (strlen(data.patientAge) > 0 && strcmp(data.patientAge,"000Y") != 0) images->addAttribute("patientAge", data.patientAge); if (data.patientSex == 'F') images->addAttribute("patientSex", "F"); else if (data.patientSex == 'M') images->addAttribute("patientSex", "M"); - if (data.patientWeight > 0.0) - images->addAttribute("patientWeight", data.patientWeight); - if (strlen(data.imageComments) > 0) - images->addAttribute("comments", data.imageComments); + images->addAttribute("patientWeight", data.patientWeight); + images->addAttribute("comments", data.imageComments); } #else @@ -3391,6 +3393,19 @@ int nii_saveNRRD(char * niiFilename, struct nifti_1_header hdr, unsigned char* i return pigz_File(fname, opts, imgsz); } // nii_saveNRRD() +#endif + +#ifdef USING_R + +#ifndef max + #define max(a,b) std::max(a,b) +#endif + +#ifndef min + #define min(a,b) std::min(a,b) +#endif + +#else #ifndef max #define max(a,b) \ @@ -3406,11 +3421,13 @@ int nii_saveNRRD(char * niiFilename, struct nifti_1_header hdr, unsigned char* i _a < _b ? _a : _b; }) #endif +#endif + void removeSclSlopeInter(struct nifti_1_header* hdr, unsigned char* img) { //NRRD does not have scl_slope scl_inter. Adjust data if possible // https://discourse.slicer.org/t/preserve-image-rescale-and-slope-when-saving-in-nrrd-file/13357 if (isSameFloat(hdr->scl_inter,0.0) && isSameFloat(hdr->scl_slope,1.0)) return; - if ((!isSameFloat(fmod(hdr->scl_inter, 1.0),0.0)) || (!isSameFloat(fmod(hdr->scl_slope, 1.0),0.0))) return; + if ((!isSameFloat(fmod(hdr->scl_inter, 1.0f),0.0)) || (!isSameFloat(fmod(hdr->scl_slope, 1.0f),0.0))) return; int nVox = 1; for (int i = 1; i < 8; i++) if (hdr->dim[i] > 1) nVox = nVox * hdr->dim[i]; @@ -3455,6 +3472,8 @@ void removeSclSlopeInter(struct nifti_1_header* hdr, unsigned char* img) { //printWarning("NRRD unable to record scl_slope/scl_inter %g/%g\n", hdr->scl_slope, hdr->scl_inter); } +#ifndef USING_R + void swapEndian(struct nifti_1_header* hdr, unsigned char* im, bool isNative) { //swap endian from big->little or little->big // must be told which is native to detect datatype and number of voxels @@ -4307,7 +4326,8 @@ float PhilipsPreciseVal (float lPV, float lRS, float lRI, float lSS) { void PhilipsPrecise(struct TDICOMdata * d, bool isPhilipsFloatNotDisplayScaling, struct nifti_1_header *h, int verbose) { if (d->manufacturer != kMANUFACTURER_PHILIPS) return; //not Philips if (d->isScaleVariesEnh) return; //issue363 rescaled before slice reordering - if (!isSameFloatGE(0.0, d->RWVScale)) { + /* + if (!isSameFloatGE(0.0, d->RWVScale)) { //https://github.com/rordenlab/dcm2niix/issues/493 h->scl_slope = d->RWVScale; h->scl_inter = d->RWVIntercept; printMessage("Using RWVSlope:RWVIntercept = %g:%g\n",d->RWVScale,d->RWVIntercept); @@ -4318,7 +4338,7 @@ void PhilipsPrecise(struct TDICOMdata * d, bool isPhilipsFloatNotDisplayScaling, printMessage(" RS = rescale slope, RI = rescale intercept, SS = scale slope\n"); printMessage(" D = R * RS + RI , P = D/(RS * SS)\n"); return; - } + }*/ if (d->intenScalePhilips == 0) return; //no Philips Precise //we will report calibrated "FP" values http://www.ncbi.nlm.nih.gov/pmc/articles/PMC3998685/ float l0 = PhilipsPreciseVal (0, d->intenScale, d->intenIntercept, d->intenScalePhilips); @@ -4476,7 +4496,7 @@ void checkSliceTiming(struct TDICOMdata * d, struct TDICOMdata * d1, int verbose while ((nSlices < kMaxEPI3D) && (d->CSA.sliceTiming[nSlices] >= 0.0)) nSlices++; if (nSlices < 1) return; - if (d->CSA.sliceTiming[kMaxEPI3D-1] < 1.0) + if (d->CSA.sliceTiming[kMaxEPI3D-1] < 1.0) printWarning("Adjusting for negative MosaicRefAcqTimes (issue 271).\n"); bool isSliceTimeHHMMSS = (d->manufacturer == kMANUFACTURER_UIH); if (isForceSliceTimeHHMMSS) isSliceTimeHHMMSS = true; @@ -4677,7 +4697,7 @@ void readSoftwareVersionsGE(char softwareVersionsGE[], int verbose,char geVersio sepStart += 1; len = 11; char * versionString = (char *)malloc(sizeof(char) * len); - versionString[len] =0; + versionString[len-1] =0; memcpy(versionString, sepStart, len); int ver1, ver2, ver3; char c1, c2, c3, c4; @@ -4717,7 +4737,7 @@ void sliceTimingGE_Testx0021x105E(struct TDICOMdata * d, struct TDCMopts opts, s float mxErr = 0.0; for (int v = 0; v < hdr->dim[3]; v++) { sliceTiming[v] = (sliceTiming[v] - mn) * 1000.0; //subtract offset, convert sec -> ms - mxErr = max(mxErr, fabs(sliceTiming[v] - d->CSA.sliceTiming[v])); + mxErr = max(mxErr, float(fabs(sliceTiming[v] - d->CSA.sliceTiming[v]))); } printMessage("Slice Timing Error between calculated and RTIA timer(0021,105E): %gms\n", mxErr); if ((mxErr < 1.0) && (opts.isVerbose < 1)) return; @@ -6168,7 +6188,7 @@ bool isSameSet (struct TDICOMdata d1, struct TDICOMdata d2, struct TDCMopts* opt if (d1.coilCrc != d2.coilCrc) { if (opts->isForceStackDCE) { if (!warnings->coilVaries) - printMessage("Slices stacked despite coil variation '%s' vs '%s'\n", d1.coilName, d2.coilName); + printMessage("Slices stacked despite coil variation '%s' vs '%s' (use '-m o' to turn off merging)\n", d1.coilName, d2.coilName); warnings->coilVaries = true; *isCoilVaries = true; } else { @@ -6189,6 +6209,7 @@ bool isSameSet (struct TDICOMdata d1, struct TDICOMdata d2, struct TDCMopts* opt warnings->nameVaries = true; return false; } + if (( *isNonParallelSlices) && (d1.CSA.mosaicSlices > 1 )) return false; //issue481 if ((!isSameFloatGE(d1.orient[1], d2.orient[1]) || !isSameFloatGE(d1.orient[2], d2.orient[2]) || !isSameFloatGE(d1.orient[3], d2.orient[3]) || !isSameFloatGE(d1.orient[4], d2.orient[4]) || !isSameFloatGE(d1.orient[5], d2.orient[5]) || !isSameFloatGE(d1.orient[6], d2.orient[6]) ) ) { if ((!warnings->orientVaries) && (!d1.isNonParallelSlices) && (!d1.isLocalizer)) @@ -6231,6 +6252,8 @@ int singleDICOM(struct TDCMopts* opts, char *fname) { struct TDICOMdata *dcmList = (struct TDICOMdata *)malloc( sizeof(struct TDICOMdata)); struct TDTI4D *dti4D = (struct TDTI4D *)malloc(sizeof(struct TDTI4D)); struct TSearchList nameList; + struct TDCMprefs prefs; + opts2Prefs (opts, &prefs); nameList.maxItems = 1; // larger requires more memory, smaller more passes nameList.str = (char **) malloc((nameList.maxItems+1) * sizeof(char *)); //reserve one pointer (32 or 64 bits) per potential file nameList.numItems = 0; @@ -6239,7 +6262,8 @@ int singleDICOM(struct TDCMopts* opts, char *fname) { nameList.numItems++; TDCMsort * dcmSort = (TDCMsort *)malloc(sizeof(TDCMsort)); dcmList[0].converted2NII = 1; - dcmList[0] = readDICOMv(nameList.str[0], opts->isVerbose, opts->compressFlag, dti4D); //ignore compile warning - memory only freed on first of 2 passes + dcmList[0] = readDICOMx(nameList.str[0], &prefs, dti4D); //ignore compile warning - memory only freed on first of 2 passes + //dcmList[0] = readDICOMv(nameList.str[0], opts->isVerbose, opts->compressFlag, dti4D); //ignore compile warning - memory only freed on first of 2 passes fillTDCMsort(dcmSort[0], 0, dcmList[0]); int ret = saveDcm2Nii(1, dcmSort, dcmList, &nameList, *opts, dti4D); freeNameList(nameList); @@ -6675,6 +6699,8 @@ int nii_loadDirCore(char *indir, struct TDCMopts* opts) { // 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; + opts2Prefs (opts, &prefs); int nConvertTotal = 0; bool compressionWarning = false; bool convertError = false; @@ -6693,8 +6719,11 @@ int nii_loadDirCore(char *indir, struct TDCMopts* opts) { convertError = true; continue; } - dcmList[i] = readDICOMv(nameList.str[i], opts->isVerbose, opts->compressFlag, dti4D); //ignore compile warning - memory only freed on first of 2 passes - //if (!dcmList[i].isValid) printf(">>>>Not a valid DICOM %s\n", nameList.str[i]); + dcmList[i] = readDICOMx(nameList.str[i], &prefs, dti4D); //ignore compile warning - memory only freed on first of 2 passes + //dcmList[i] = readDICOMv(nameList.str[i], opts->isVerbose, opts->compressFlag, dti4D); //ignore compile warning - memory only freed on first of 2 passes + if (opts->isIgnoreSeriesInstanceUID) + dcmList[i].seriesUidCrc = dcmList[i].seriesNum; + //if (!dcmList[i].isValid) printf(">>>>Not a valid DICOM %s\n", nameList.str[i]); if ((dcmList[i].isValid) && ((dti4D->sliceOrder[0] >= 0) || (dcmList[i].CSA.numDti > 1))) { //4D dataset: dti4D arrays require huge amounts of RAM - write this immediately struct TDCMsort dcmSort[1]; fillTDCMsort(dcmSort[0], i, dcmList[i]); @@ -6737,8 +6766,8 @@ int nii_loadDirCore(char *indir, struct TDCMopts* opts) { bool matched = false; // If the file matches an existing series, add it to the corresponding file list for (int j = 0; j < opts->series.size(); j++) { - bool isMultiEchoUnused, isNonParallelSlices, isCoilVaries; - if (isSameSet(opts->series[j].representativeData, dcmList[i], opts, &warnings, &isMultiEchoUnused, &isNonParallelSlices, &isCoilVaries)) { + bool isMultiEcho = false, isNonParallelSlices = false, isCoilVaries = false; + if (isSameSet(opts->series[j].representativeData, dcmList[i], opts, &warnings, &isMultiEcho, &isNonParallelSlices, &isCoilVaries)) { opts->series[j].files.push_back(nameList.str[i]); matched = true; break; @@ -6776,8 +6805,8 @@ int nii_loadDirCore(char *indir, struct TDCMopts* opts) { nConvert = 0; for (int j = i; j < (int)nDcm; j++) { isMultiEcho = false; - isNonParallelSlices = false; isCoilVaries = false; + isNonParallelSlices = false; if (isSameSet(dcmList[i], dcmList[j], opts, &warnings, &isMultiEcho, &isNonParallelSlices, &isCoilVaries)) { dcmList[j].converted2NII = 1; //do not reprocess repeats fillTDCMsort(dcmSort[nConvert], j, dcmList[j]); @@ -6840,6 +6869,25 @@ int nii_loadDirCore(char *indir, struct TDCMopts* opts) { nConvert++; } } //for all images with same seriesUID as first one + 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++) { + int ji = crcSort[j].indx; + if (dcmList[ii].seriesUidCrc != dcmList[ji].seriesUidCrc) break; + dcmList[ji].converted2NII = 1; + dcmList[ji].isNonParallelSlices = true; + if (isMultiEcho) dcmList[ji].isMultiEcho = true; + if (isCoilVaries) dcmList[ji].isCoilVaries = true; + struct TDCMsort dcmSort[1]; + fillTDCMsort(dcmSort[0], ji, dcmList[ji]); + int ret = saveDcm2Nii(1, dcmSort, dcmList, &nameList, *opts, dti4D); + if (ret == EXIT_SUCCESS) + nConvertTotal++; + else + convertError = true; + } + continue; + } //issue481 //issue 381: ensure all images are informed if there are variations in echo, parallel slices, coil name: if (isMultiEcho) for (int j = i; j <= jMax; j++) { @@ -7214,6 +7262,7 @@ void setDefaultOpts (struct TDCMopts *opts, const char * argv[]) { //either "set opts->isRenameNotConvert = false; opts->isForceStackSameSeries = 2; //automatic: stack CTs, do not stack MRI opts->isForceStackDCE = true; + opts->isIgnoreSeriesInstanceUID = false; opts->isIgnoreDerivedAnd2D = false; opts->isForceOnsetTimes = true; opts->isPhilipsFloatNotDisplayScaling = true; @@ -7223,6 +7272,7 @@ void setDefaultOpts (struct TDCMopts *opts, const char * argv[]) { //either "set 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->isIgnoreTriggerTimes = false; opts->isSaveNRRD = false; opts->isPipedGz = false; //e.g. pipe data directly to pigz instead of saving uncompressed to disk opts->isSave3D = false; diff --git a/console/nii_dicom_batch.h b/console/nii_dicom_batch.h index e8cad56e..d62c16b1 100644 --- a/console/nii_dicom_batch.h +++ b/console/nii_dicom_batch.h @@ -35,7 +35,7 @@ extern "C" { #define MAX_NUM_SERIES 16 struct TDCMopts { - bool isTestx0021x105E, isAddNamePostFixes, isSaveNativeEndian, isSaveNRRD, isOneDirAtATime, isRenameNotConvert, isSave3D, isGz, isPipedGz, isFlipY, isCreateBIDS, isSortDTIbyBVal, isAnonymizeBIDS, isOnlyBIDS, isCreateText, isForceOnsetTimes,isIgnoreDerivedAnd2D, isPhilipsFloatNotDisplayScaling, isTiltCorrect, isRGBplanar, isOnlySingleFile, isForceStackDCE, isRotate3DAcq, isCrop; + bool isIgnoreTriggerTimes, isTestx0021x105E, isAddNamePostFixes, isSaveNativeEndian, isSaveNRRD, isOneDirAtATime, isRenameNotConvert, isSave3D, isGz, isPipedGz, isFlipY, isCreateBIDS, isSortDTIbyBVal, isAnonymizeBIDS, isOnlyBIDS, isCreateText, isForceOnsetTimes,isIgnoreDerivedAnd2D, isPhilipsFloatNotDisplayScaling, isTiltCorrect, isRGBplanar, isOnlySingleFile, isForceStackDCE, isIgnoreSeriesInstanceUID, isRotate3DAcq, isCrop; int isMaximize16BitRange, isForceStackSameSeries, nameConflictBehavior, isVerbose, isProgress, compressFlag, dirSearchDepth, gzLevel; //support for compressed data 0=none, char filename[512], outdir[512], indir[512], pigzname[512], optsname[512], indirParent[512], imageComments[24]; double seriesNumber[MAX_NUM_SERIES]; //requires double must store -1 (report but do not convert) as well as seriesUidCrc (uint32) diff --git a/dcm_qa_nih b/dcm_qa_nih index a6fffa39..0a0aa943 160000 --- a/dcm_qa_nih +++ b/dcm_qa_nih @@ -1 +1 @@ -Subproject commit a6fffa392766b7bd816fe0e822e6469f044af9ee +Subproject commit 0a0aa943b5e5263645a33359e52dedf147c6a5a7