OrientImageFilter changes the origin of the image

I can reproduce it:

Original Orientation: LPI
Started verification.
Original Image Properties:
        Origin: [86.9175, 90.0284, -101.794]
Oriented Image [RAI] Properties:
        Origin: [-100.04, -158.669, -101.794]
Oriented Image [ORIGINAL] Properties:
        Origin: [86.9378, 90.0131, -101.794]
Finished successfully.

but it doesn’t occur with some other image I had lying around:

Original Orientation: RAI
Started verification.
Original Image Properties:
        Origin: [-127.5, -127.5, -127.5]
Oriented Image [RAI] Properties:
        Origin: [-127.5, -127.5, -127.5]
Oriented Image [ORIGINAL] Properties:
        Origin: [-127.5, -127.5, -127.5]
Finished successfully.

so it seems to be related to something specific in your image. I am investigating.

1 Like

Is your image isotropic? Perhaps that might be a source of difference. In any case, thank you for looking into it! I am looking forward to hearing from you.

1 Like

I was just poking at this issue.

That is the issue! Look at this code in PermuteAxesImageFilter:

For the case of anisotropic voxels the origin should change! While the location for the corner of the voxel should remains the same, the location of the center is suppose to change for the PermuteImageFiler.

1 Like

Thanks for the response!

I understand that the origins should change for anisotropic images but when I change the image back to the original orientation, should it not be preserved? From the example I have shown (and verified by @dzenanz), there is a minor difference in the final re-oriented and original images.

The above code is from the PermuteImageFilter which is used by the OrientImageFilter. I believe it is the cause of the bug you are seeing as it does not correctly compute origin after the permutation of the order of the axes.

Ah, I understand. I got thrown off by the fact that the origin changed for me.

Edited: typo

FYI, my test, original image, (BTW it is signed short image, but opened as float to reproduce above test:

r@deb:~/tmp/test038/build$ ./test038 original.nii.gz 
original LPI [86.9175, 90.0284, -101.794]
filter output LPI->RAI [-100.04, -158.669, -101.794]
filter output RAI->LPI [86.9175, 90.0284, -101.794]
re-opened LPI->RAI [-100.04, -158.669, -101.794]
re-opened RAI->LPI [86.9175, 90.0284, -101.794]

Here is code

#include "itkImage.h"
#include "itkImageFileReader.h"
#include "itkImageFileWriter.h"
#include "itkSpatialOrientation.h"
#include "itkOrientImageFilter.h"

template<typename T> bool orient_filter(
  const typename T::Pointer & image,
  typename T::Pointer & out_image,
  int f)
{
  if (image.IsNull()) return false;
  typedef itk::OrientImageFilter<T,T> OrientImageFilterType;
  typename OrientImageFilterType::Pointer filter = OrientImageFilterType::New();
  try
  {
    filter->SetInput(image);
    filter->UseImageDirectionOn();
    filter->SetDesiredCoordinateOrientation(
      static_cast<itk::SpatialOrientation::ValidCoordinateOrientationFlags>(f));
    filter->Update();
    out_image = filter->GetOutput();
  }
  catch (itk::ExceptionObject & ex)
  {
    std::cout << ex.GetDescription() << std::endl;
    return false;
  }
  if (out_image.IsNotNull()) out_image->DisconnectPipeline();
  else return false;
  return true;
}

int main(int argc, char ** argv)
{
  typedef itk::Image<float, 3> ImageType; 
  typedef itk::ImageFileReader<ImageType> ReaderType;
  typedef itk::ImageFileWriter<ImageType> WriterType;
  ImageType::Pointer inputLPI;
  ImageType::Pointer outputRAI;
  ImageType::Pointer outputLPI;
  ImageType::Pointer reopenedRAI;
  ImageType::Pointer reopenedLPI;
  ReaderType::Pointer reader = ReaderType::New();
  reader->SetFileName(argv[1]);
  try
  {
    reader->Update();
    inputLPI = reader->GetOutput();
  }
  catch (itk::ExceptionObject & ex)
  {
    std::cout << ex.GetDescription() << std::endl;
    return 1;
  }
  std::cout << "original LPI " << inputLPI->GetOrigin()
    << std::endl;
  orient_filter<ImageType>(
    inputLPI,
    outputRAI,
    (int)itk::SpatialOrientation::ITK_COORDINATE_ORIENTATION_RAI);
  std::cout << "filter output LPI->RAI " << outputRAI->GetOrigin()
    << std::endl;
  orient_filter<ImageType>(
    outputRAI,
    outputLPI,
    (int)itk::SpatialOrientation::ITK_COORDINATE_ORIENTATION_LPI);
  std::cout << "filter output RAI->LPI " << outputLPI->GetOrigin()
    << std::endl;
  WriterType::Pointer writer1 = WriterType::New();
  WriterType::Pointer writer2 = WriterType::New();
  writer1->SetInput(outputRAI);
  writer1->SetFileName("outputRAI.nii.gz");
  writer2->SetInput(outputLPI);
  writer2->SetFileName("outputLPI.nii.gz");
  try
  {
    writer1->Update();
    writer2->Update();
  }
  catch (itk::ExceptionObject & ex)
  {
    std::cout << ex.GetDescription() << std::endl;
    return 1;
  }
  ReaderType::Pointer reader1 = ReaderType::New();
  ReaderType::Pointer reader2 = ReaderType::New();
  reader1->SetFileName("outputRAI.nii.gz");
  reader2->SetFileName("outputLPI.nii.gz");
  try
  {
    reader1->Update();
    reader2->Update();
    reopenedRAI = reader1->GetOutput();
    reopenedLPI = reader2->GetOutput();
  }
  catch (itk::ExceptionObject & ex)
  {
    std::cout << ex.GetDescription() << std::endl;
    return 1;
  }
  std::cout << "re-opened LPI->RAI " << reopenedRAI->GetOrigin()
    << std::endl;
  std::cout << "re-opened RAI->LPI n" << reopenedLPI->GetOrigin()
    << std::endl;
  return 0;
}

nnn

The other image I have is isotropic (spacing 1.0).

I created an issue to track this. Do we have a course of action regarding this? Are @blowekamp or @mihail.isakov working on a fix already? If yes, please assign the issue to yourself.

I still don’t see the issue, change orientation filter works well for me, s. test above, not sure, may i be oversee something, of course, example is a little too long for me dig into just now, sorry.

After trying to replicate what @mihail.isakov has done, I tried doing the re-conversion of the RAI image from both the itk::Image Pointer and the saved image on disk [ref] and it turns out, there are differences when the file is written from the disk (see output):

Original Orientation: LPI
Started verification.
Original Image Properties:
        Origin: [86.9175, 90.0284, -101.794]
Oriented Image [RAI] Properties:
        Origin: [-100.04, -158.669, -101.794]
Re-Oriented Image [LPI] - from Raw RAI Properties:
        Origin: [86.9175, 90.0284, -101.794]
Re-Oriented Image [LPI] - from Written RAI Properties:
        Origin: [86.9378, 90.0131, -101.794]
1 Like

Here is my untested fix:

If someone else case write a test, it would be much appreciated.

1 Like

I pulled these fixes but the problem persists:

Original Orientation: LPI
Started verification.
Original Image Properties:
        Origin: [86.9175, 90.0284, -101.794]
Oriented Image [RAI] Properties:
        Origin: [-100.04, -158.669, -101.794]
Re-Oriented Image [LPI] - from Raw RAI Properties:
        Origin: [86.9175, 90.0284, -101.794]
Re-Oriented Image [LPI] - from Written RAI Properties:
        Origin: [86.9378, 90.0131, -101.794]

I believe the issue is coming from the way the writer is getting called (though I have no idea why). I am trying to find the direction cosines of the same image here, one right after the orientation function and another after I have written and read it back from disk:

Oriented Image [RAI] Raw Properties:
        Origin: [-100.04, -158.669, -101.794]
        Direction Cosines:
0.999998 0.00174533 0
-0.00174533 0.999998 0
0 0 1

Oriented Image [RAI] FromFile Properties:
        Origin: [-100.04, -158.669, -101.794]
        Direction Cosines:
0.999998 0.00182698 0
-0.00182698 0.999998 0
0 0 1

Thank you for looking into, interesting. I will do more tests with orientation filter, probably i’ll modify my example to apply a number of random re-orientations, e.g. 20 or so and look at origin, spacing, direction, without writing. I hope the results will be OK. I’ll post.
I use Nifti IO as ITK offers, no time to dig into, may be there is something with quaternions math, just guessing.

After writing some tests to verify the geometry for the PermuteImageFilter, I determined that the existing computation of the origin is correct.

1 Like

I tried doing the re-conversion of the RAI image from both the itk::Image Pointer and the saved image on disk [ref ] and it turns out, there are differences when the file is written from the disk

I am able to reproduce this using multiple anisotropic images. I used the code that @mihail.isakov gave with same results (ref).

Thanks for all the help @dzenanz @blowekamp @mihail.isakov

So, the issue is actually in the NIfTI IO class. I tried writing the files using different formats (mha, nrrd, nii, img+hdr) and all formats associated with that class had the same issue (ref). To work around this issue, I have decided to use MHA/NRRD for now.

1 Like

BTW, this issue is present in the VTK format as well (ref).

Yes, you are right, i can reproduce the issue with Nifti IO. Here is test, no image is required to load, is created.

r@deb:~/tmp/test077/build$ ./test077
*** Origin [-100.04, -158.669, -101.794]
Direction
0.999998 0.00174533 0
-0.00174533 0.999998 0
0 0 1

*** Re-opened
Origin [-100.04, -158.669, -101.794]
Direction
0.999998 0.00182698 0
-0.00182698 0.999998 0
0 0 1

#include "itkImage.h"
#include "itkImageFileWriter.h"
#include "itkImageFileReader.h"

int main(int argc, char ** argv)
{
  typedef itk::Image<float, 3> ImageType; 
  typedef itk::ImageFileWriter<ImageType> WriterType;
  typedef itk::ImageFileReader<ImageType> ReaderType;
  {
    ImageType::Pointer image = ImageType::New();
    ImageType::IndexType index;
    ImageType::SizeType size;
    ImageType::RegionType region;
    ImageType::SpacingType spacing;
    ImageType::PointType origin;
    ImageType::DirectionType direction;
    index.Fill(0);
    size[0] = 192;
    size[1] = 256;
    size[2] = 192;
    spacing[0] = 0.976562;
    spacing[1] = 0.976562;
    spacing[2] = 1;
    origin[0] = -100.04;
    origin[1] = -158.669;
    origin[2] = -101.794;
    direction(0,0) = 0.999998;
    direction(0,1) = 0.00174533;
    direction(0,2) = 0;
    direction(1,0) = -0.00174533;
    direction(1,1) = 0.999998;
    direction(1,2) = 0;
    direction(2,0) = 0;
    direction(2,1) = 0;
    direction(2,2) = 1;
    region.SetIndex(index);
    region.SetSize(size);
    try
    {
      image->SetRegions(region);
      image->Allocate();
      image->SetOrigin(origin);
      image->SetSpacing(spacing);
      image->SetDirection(direction);
    }
    catch (itk::ExceptionObject & ex)
    {
      std::cout << ex.GetDescription() << std::endl;
      return 1;
    }
    std::cout << "*** Origin " << image->GetOrigin() << std::endl;
    std::cout << "Direction\n" << image->GetDirection() << std::endl;
    WriterType::Pointer writer = WriterType::New();
    writer->SetInput(image);
    writer->SetFileName("output.nii.gz"); // MHA is OK
    try
    {
      writer->Write();
    }
    catch (itk::ExceptionObject & ex)
    {
      std::cout << ex.GetDescription() << std::endl;
      return 1;
    }
  }

  {
    ImageType::Pointer reopened;
    ReaderType::Pointer reader = ReaderType::New();
    reader->SetFileName("output.nii.gz"); // MHA is OK
    try
    {
      reader->Update();
      reopened = reader->GetOutput();
    }
    catch (itk::ExceptionObject & ex)
    {
      std::cout << ex.GetDescription() << std::endl;
      return 1;
    }
    std::cout << "*** Re-opened\nOrigin " << reopened->GetOrigin() << std::endl;
    std::cout << "Direction\n" << reopened->GetDirection() << std::endl;
  }
  return 0;
}

I didn’t looked closer, but could not reproduce with open/write Nifti images, may be, not sure, once written as Nifti image the image has matrix somehow compatible with computations, but not every matrix, the image above is valid, BTW, there should be no reason for mistake, IMHO. Don’t know, but there is something, there is also the issue about "round trip problems writing some DICOM files to Nifti and back, it is the same, may be.

P.S. As @blowekamp mentioned, orientation filter is absolutely OK, i have played with this testand geometry survived 500 random re-orientation. I think it is good news, IMHO.

1 Like

I created a new issue to track this. I also closed the old one.

3 Likes