Nifti file orientation change between 2019-08-26 and ITK v5.1rc01

We are updating 3D Slicer to use ITK v5.1rc01 and two of our tests now fail. The problem is that a nifti file loads with different image directions now compared to previously:

  • ITK master as of 2019-08-26 => image directions [-1 0 0; 0 -1 0; 0 0 1]
  • ITK v5.1rc01 => image directions [1 0 0; 0 -1 0; 0 0 1]

Here is the nifti file: OAS10001.hdr, OAS10001.img. It is originally downloaded from the OASIS database (probably it is this image).

Most probably the behavior change is caused by this commit. While some explanation for the change is provided in the commit comment, I’m not sure if it is a good enough justification for suddenly changing fundamental image loading behavior in ITK that has been the same for 11 years (the test was added to Slicer in since 2009 and it has been successfully passing until now).

I avoid nifti file format as much as possible (mostly because of these seemingly never-ending problems around how their orientation is defined), so I don’t mind if interpretation of their orientation takes another twist (we can just switch to another data set). However, such behavior change may break many other projects.

I would recommend to reconsider this behavior change and if the decision is made to keep the new behavior then take steps to minimize chance of causing serious problems in various projects (warning in release notes, runtime warning message, configuration option to retain previous behavior, etc. could help).

3 Likes

I agree with @lassoan, this change in behavior is likely to cause trouble.

Looking at the linked commit, it seems the intention was to make reading of legacy analyze files consistent with some previous interpretation at one site. But the entire motivation for nifti was to address the problem that analyze files did not have a well accepted meaning with respect to orientation. Changing ITK’s behavior is only going to make the bad situation worse.

Previously, 3D Slicer and ITK-Snap displayed the OAS10001 data consistently with each other, but incorrectly (patient face is towards P direction):

WIth latest latest ITK master, the image is loaded correctly:

By the way, with current ITK master, the IJK to LPS matrix is left-handed - diag(1,-1,1) - which could cause various issues. It could be fixed by reordering slices and it would be nice if ITK could do it, but it is manageable at application level, too.

Since there is no one good option (you either cause a serious breaking change or load some images incorrectly), the behavior should not be hardcoded in ITK. A safe solution would be display a warning when ambiguous nifti image is encountered and allow users to choose loading behavior in the nifti reader. One flag could be added to choose old/new behavior and another flag to suppress the warning message.

1 Like

@Vladimir_Fonov @seanm please pitch in.

See also this classic Analyze (Not NIfTI) test file, that has burned in RL text indicating how the image (with Analyze orientation of 0 (TRANSVERSE_UNFLIPPED) and positive pixdim values) “should” (?) be displayed:

http://web.archive.org/web/20060108143626/http://www.fmrib.ox.ac.uk/~steve/ftp/avg152T1_LR-marked.tar.gz

This avg152T1 data set loads incorrectly with both the old and new ITK behavior:

Old (both LR and AP axes are incorrectly oriented):

New (only LR axis is incorreclty oriented):

It seems that Nifty/Analyze is kind of hopeless… Since there does not seem to be a chance to consistently get correct orientation for all images, probably the best is to keep the old behavior by default and allow the proposed new behavior (which could fix some orientation errors on some images, at the cost of breaking backward compatibility) as an option.

2 Likes

what’s the contents of the header of avg152T1 ?

If you can recommend a Windows tool that can dump the header to a text file then I can copy the results here. Otherwise, you have to download the file from the URL above.

[main] INFO com.pixelmed.convert.AnalyzeHeader Is BigEndian

[main] INFO com.pixelmed.convert.AnalyzeHeader Have Analyze header

[main] INFO com.pixelmed.convert.AnalyzeHeader dim[0] = 4

[main] INFO com.pixelmed.convert.AnalyzeHeader dim[1] = 91

[main] INFO com.pixelmed.convert.AnalyzeHeader dim[2] = 109

[main] INFO com.pixelmed.convert.AnalyzeHeader dim[3] = 91

[main] INFO com.pixelmed.convert.AnalyzeHeader dim[4] = 1

[main] INFO com.pixelmed.convert.AnalyzeHeader dim[5] = 0

[main] INFO com.pixelmed.convert.AnalyzeHeader dim[6] = 0

[main] INFO com.pixelmed.convert.AnalyzeHeader dim[7] = 0

[main] INFO com.pixelmed.convert.AnalyzeHeader unused8 = 28013

[main] INFO com.pixelmed.convert.AnalyzeHeader unused9 = 0

[main] INFO com.pixelmed.convert.AnalyzeHeader unused10 = 0

[main] INFO com.pixelmed.convert.AnalyzeHeader unused11 = 0

[main] INFO com.pixelmed.convert.AnalyzeHeader unused12 = 0

[main] INFO com.pixelmed.convert.AnalyzeHeader unused13 = 0

[main] INFO com.pixelmed.convert.AnalyzeHeader unused14 = 0

[main] INFO com.pixelmed.convert.AnalyzeHeader datatype = 2 UNSIGNED_CHAR

[main] INFO com.pixelmed.convert.AnalyzeHeader bitpix = 8

[main] INFO com.pixelmed.convert.AnalyzeHeader dim_un0 = 0

[main] INFO com.pixelmed.convert.AnalyzeHeader pixdim[0] = 0.0

[main] INFO com.pixelmed.convert.AnalyzeHeader pixdim[1] = -2.0

[main] INFO com.pixelmed.convert.AnalyzeHeader pixdim[2] = 2.0

[main] INFO com.pixelmed.convert.AnalyzeHeader pixdim[3] = 2.0

[main] INFO com.pixelmed.convert.AnalyzeHeader pixdim[4] = 0.0

[main] INFO com.pixelmed.convert.AnalyzeHeader pixdim[5] = 0.0

[main] INFO com.pixelmed.convert.AnalyzeHeader pixdim[6] = 0.0

[main] INFO com.pixelmed.convert.AnalyzeHeader pixdim[7] = 0.0

[main] INFO com.pixelmed.convert.AnalyzeHeader vox_offset = 0.0

[main] INFO com.pixelmed.convert.AnalyzeHeader funused1 = 1715.0446

[main] INFO com.pixelmed.convert.AnalyzeHeader funused2 = 0.0

[main] INFO com.pixelmed.convert.AnalyzeHeader funused3 = 0.0

[main] INFO com.pixelmed.convert.AnalyzeHeader cal_max = 0.0

[main] INFO com.pixelmed.convert.AnalyzeHeader cal_min = 0.0

[main] INFO com.pixelmed.convert.AnalyzeHeader compressed = 0.0

[main] INFO com.pixelmed.convert.AnalyzeHeader verified = 0.0

[main] INFO com.pixelmed.convert.AnalyzeHeader glmax = 255

[main] INFO com.pixelmed.convert.AnalyzeHeader glmin = 0

[main] INFO com.pixelmed.convert.AnalyzeHeader description = ICBM AVG 152 T1 TAL LIN

[main] INFO com.pixelmed.convert.AnalyzeHeader aux_file = none

[main] INFO com.pixelmed.convert.AnalyzeHeader orient = 0 TRANSVERSE_UNFLIPPED

[main] INFO com.pixelmed.convert.AnalyzeHeader originator = .@%

[main] INFO com.pixelmed.convert.AnalyzeHeader generated =

[main] INFO com.pixelmed.convert.AnalyzeHeader scannum =

[main] INFO com.pixelmed.convert.AnalyzeHeader patient_id =

[main] INFO com.pixelmed.convert.AnalyzeHeader exp_date =

[main] INFO com.pixelmed.convert.AnalyzeHeader exp_time =

[main] INFO com.pixelmed.convert.AnalyzeHeader hist_un0 =

[main] INFO com.pixelmed.convert.AnalyzeHeader views = 0

[main] INFO com.pixelmed.convert.AnalyzeHeader vols_added = 0

[main] INFO com.pixelmed.convert.AnalyzeHeader start_field = 0

[main] INFO com.pixelmed.convert.AnalyzeHeader field_skip = 0

[main] INFO com.pixelmed.convert.AnalyzeHeader omax = 0

[main] INFO com.pixelmed.convert.AnalyzeHeader omin = 0

[main] INFO com.pixelmed.convert.AnalyzeHeader smax = 0

[main] INFO com.pixelmed.convert.AnalyzeHeader smin = 0

Ok, so , just to clarify - I tried restoring the original behaviour of Analyze reader , from ITK 4.XX, that got obsoleted by NIFTI reader: https://github.com/InsightSoftwareConsortium/ITK/blob/release-4.13/Modules/Compatibility/Deprecated/src/itkAnalyzeImageIO.cxx#L1033

OK, so the flag in nifti reader could be called “AnalyzeCompatibility” or something like that. It could be also possible to keep around a simple AnalyzeImageIO class, which would be derived from NiftiImageIO and it would set the AnalyzeCompatibility flag to true in its constructor and otherwise it would be empty.

nifti_tool -disp_ana -infile avg152T1_LR-marked.hdr
...
pixdim                76      8    0.0 -2.0 2.0 2.0 0.0 0.0 0.0 0.0
...
orient               252      1    0
...

So, based on what http://www.grahamwideman.com/gw/brain/analyze/formatdoc.htm says

the actual interpretation of what it means depends on the software (i.e FSL ignores negative dimension sizes, and SPM would flip voxel order ).

Maybe then we could add an enum instead of flag to specify file format variant: Nifti, AnalyzeFSL, AnalyzeSPM.

I don’t think there is a way to find out which flavour of Analyze it is.

Exactly. That’s why the behavior would not be hardcoded in the IO class but it would be an enum that applications can set (based on application preferences, user choice, some heuristics, etc.).

OK, but ITK 5.0 was neither SPM or FSL, so it would be AnalyzeITK5.0 ?

In Slicer, ITK has been reading img+hdr files for 10+ years the same way. For us, nothing has changed until your recent commit. Maybe the behavior depended on how you initialized ITK image IO factories and you somehow ended up using AnalyzeImageIO, while Slicer always ended up using NiftiImageIO.

The file format variant can be called Nifti, AnalyzeITK5, etc., whatever folks who use Nifty/Analyze find the most informative.

1 Like

Clarification: These images are NOT nifti formatted images. They are ancient analyze formatted images, and there is/has always been some irreconcilable ambiguities how those analyze files were written.

The FSL/SPM/ and various laboratories interpret the field names differently. The NIFTI format attempted to address and document one accepted format (for which it partially succeeded).

Regarding the commit that was provided, I think it should be reverted in ITK. Only 1/2 of the community will be satisfied with any default value that is chosen for reading these ancient analyze files.

1 Like

The ITK Analyze Reader was deprecated since 9 years ago. But it was used when ITKV3_COMPATIBILITY was enabled in ITK 4.XX , I think.

Maybe we can introduce ITKV3_ANALYZE_COMPATIBILITY Cmake Flag?

Here is my suggestion.

Add a flag to the NIFTIIO reader that is based on the following enumeration:

enum class Analyze75FormatCompatibility
{
REJECT=0, //NiftiIO will not read analyze 7.5 formatted files
FSL_CONVENTIONS=1, //NiftiIO will read and use implied directions based on old FSL conventions
SPM_CONVENTIONS=2 //NiftiIO will read and use implied directions based on old SPM/AFNI conventions
}

If the image does not have the “magic” string at byte #344 that indicates that it is a modern nifti file (i.e. it is an old Analyze file), then ITK should NOT READ THE FILE BY DEFAULT, and the CanRead() function should return false.

I don’t have time to work on this at the moment.

NOTE: This was discussed/argued about extensively, and the current behavior was weakly agreed upon nearly a decade ago. I prefer “breakage with explicit messaging” to quiet behavior change.

2 Likes