Skip to content

Commit e3e48cb

Browse files
committed
ENH: Support genuine 4D (x,y,z,time) MINC images
The MINC time dimension now maps to a 4th ITK dimension; the vector_dimension maps to components. Component count no longer selects the written dimension kind, so time and vector survive a round-trip. Change-Id: I155561693ee98c5677b2dfa125fc9990e7721bb2
1 parent 31b64bf commit e3e48cb

5 files changed

Lines changed: 135 additions & 68 deletions

File tree

Modules/IO/MINC/include/itkMINCImageIO.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,12 @@ class ITKIOMINC_EXPORT MINCImageIO : public ImageIOBase
8585
/** \see LightObject::GetNameOfClass() */
8686
itkOverrideGetNameOfClassMacro(MINCImageIO);
8787

88-
/** Right now MINC supports up to 3D with multiple components */
88+
/** MINC supports up to 3 spatial dimensions plus a time dimension (4D),
89+
* each with multiple components (MINC vector_dimension). */
8990
bool
9091
SupportsDimension(unsigned long dim) override
9192
{
92-
return dim < 4;
93+
return dim < 5;
9394
}
9495

9596
/*-------- This part of the interface deals with reading data. ------ */

Modules/IO/MINC/src/itkMINCImageIO.cxx

Lines changed: 60 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -440,12 +440,14 @@ MINCImageIO::ReadImageInformation()
440440
itkExceptionStringMacro(" minc files without spatial dimensions are not supported!");
441441
}
442442

443-
if (m_MINCPImpl->m_DimensionIndices[0] != -1 && m_MINCPImpl->m_DimensionIndices[4] != -1)
444-
{
445-
itkExceptionStringMacro(" 4D minc files vector dimension are not supported currently");
446-
}
443+
const bool haveTimeDimension = (m_MINCPImpl->m_DimensionIndices[4] != -1);
447444

448-
this->SetNumberOfDimensions(spatial_dimension_count);
445+
// The MINC time dimension becomes an additional (highest) ITK dimension, while
446+
// the MINC vector_dimension is mapped to ITK components. The two are
447+
// independent and may coexist (e.g. a 4D vector image).
448+
const unsigned int itkDimensionCount = spatial_dimension_count + (haveTimeDimension ? 1 : 0);
449+
450+
this->SetNumberOfDimensions(itkDimensionCount);
449451

450452
int numberOfComponents = 1;
451453
unsigned int usableDimensions = 0;
@@ -457,11 +459,37 @@ MINCImageIO::ReadImageInformation()
457459
auto RAStofromLPS = Matrix<double, 3, 3>::GetIdentity();
458460
RAStofromLPS(0, 0) = -1.0;
459461
RAStofromLPS(1, 1) = -1.0;
460-
std::vector<double> dir_cos_temp(3);
462+
std::vector<double> dir_cos_temp(itkDimensionCount, 0.0);
461463

462464
Vector<double, 3> origin{};
463465
Vector<double, 3> oOrigin{};
464466

467+
// Build the MINC apparent dimension order slowest-varying first. The time
468+
// dimension is the slowest, matching the ITK buffer layout used by
469+
// Read()/Write() where it is the highest ITK dimension.
470+
if (haveTimeDimension)
471+
{
472+
m_MINCPImpl->m_MincApparentDims[usableDimensions] = m_MINCPImpl->m_MincFileDims[m_MINCPImpl->m_DimensionIndices[4]];
473+
// always use positive
474+
miset_dimension_apparent_voxel_order(m_MINCPImpl->m_MincApparentDims[usableDimensions], MI_POSITIVE);
475+
misize_t _sz = 0;
476+
miget_dimension_size(m_MINCPImpl->m_MincApparentDims[usableDimensions], &_sz);
477+
double _sep = NAN;
478+
miget_dimension_separation(m_MINCPImpl->m_MincApparentDims[usableDimensions], MI_ORDER_APPARENT, &_sep);
479+
double _start = NAN;
480+
miget_dimension_start(m_MINCPImpl->m_MincApparentDims[usableDimensions], MI_ORDER_APPARENT, &_start);
481+
482+
this->SetDimensions(spatial_dimension_count, static_cast<unsigned int>(_sz));
483+
this->SetSpacing(spatial_dimension_count, _sep);
484+
this->SetOrigin(spatial_dimension_count, _start);
485+
486+
std::vector<double> time_dir(itkDimensionCount, 0.0);
487+
time_dir[spatial_dimension_count] = 1.0;
488+
this->SetDirection(spatial_dimension_count, time_dir);
489+
490+
++usableDimensions;
491+
}
492+
465493
// minc api uses inverse order of dimensions , fastest varying are last
466494
Vector<double, 3> sep;
467495
for (int i = 3; i > 0; i--)
@@ -511,7 +539,7 @@ MINCImageIO::ReadImageInformation()
511539
for (int i = 0; i < spatial_dimension_count; ++i)
512540
{
513541
this->SetOrigin(i, oOrigin[i]);
514-
for (unsigned int j = 0; j < 3; j++)
542+
for (unsigned int j = 0; j < static_cast<unsigned int>(spatial_dimension_count); ++j)
515543
{
516544
dir_cos_temp[j] = dir_cos[j][i];
517545
}
@@ -530,18 +558,6 @@ MINCImageIO::ReadImageInformation()
530558
++usableDimensions;
531559
}
532560

533-
if (m_MINCPImpl->m_DimensionIndices[4] != -1) // have time dimension
534-
{
535-
// micopy_dimension(hdim[m_MINCPImpl->m_DimensionIndices[4]],&apparent_dimension_order[usable_dimensions]);
536-
m_MINCPImpl->m_MincApparentDims[usableDimensions] = m_MINCPImpl->m_MincFileDims[m_MINCPImpl->m_DimensionIndices[4]];
537-
// always use positive
538-
miset_dimension_apparent_voxel_order(m_MINCPImpl->m_MincApparentDims[usableDimensions], MI_POSITIVE);
539-
misize_t _sz = 0;
540-
miget_dimension_size(m_MINCPImpl->m_MincApparentDims[usableDimensions], &_sz);
541-
numberOfComponents = _sz;
542-
++usableDimensions;
543-
}
544-
545561
// Set apparent dimension order to the MINC2 api
546562
if (miset_apparent_dimension_order(m_MINCPImpl->m_Volume, usableDimensions, m_MINCPImpl->m_MincApparentDims) < 0)
547563
{
@@ -893,33 +909,9 @@ MINCImageIO::WriteImageInformation()
893909
MetaDataDictionary & thisDic = GetMetaDataDictionary();
894910

895911
unsigned int minc_dimensions = 0;
896-
if (nComp > 3) // last dimension will be either vector or time
897-
{
898-
micreate_dimension(MItime,
899-
MI_DIMCLASS_TIME,
900-
MI_DIMATTR_REGULARLY_SAMPLED,
901-
nComp,
902-
&m_MINCPImpl->m_MincApparentDims[m_MINCPImpl->m_NDims - minc_dimensions - 1]);
903-
904-
double tstart = 0.0;
905-
if (!ExposeMetaData<double>(thisDic, "tstart", tstart))
906-
{
907-
tstart = 0.0;
908-
}
909-
910-
miset_dimension_start(m_MINCPImpl->m_MincApparentDims[m_MINCPImpl->m_NDims - minc_dimensions - 1], tstart);
911-
912-
double tstep = 1.0;
913-
if (!ExposeMetaData<double>(thisDic, "tstep", tstep))
914-
{
915-
tstep = 1.0;
916-
}
917-
918-
miset_dimension_separation(m_MINCPImpl->m_MincApparentDims[m_MINCPImpl->m_NDims - minc_dimensions - 1], tstep);
919-
920-
++minc_dimensions;
921-
}
922-
else if (nComp > 1)
912+
// ITK components are always written as a MINC vector_dimension (fastest
913+
// varying). A time series is an ITK dimension, not components (see below).
914+
if (nComp > 1)
923915
{
924916
micreate_dimension(MIvector_dimension,
925917
MI_DIMCLASS_RECORD,
@@ -956,10 +948,20 @@ MINCImageIO::WriteImageInformation()
956948
++minc_dimensions;
957949
}
958950

959-
if (nDims > 3)
951+
if (nDims > 4)
960952
{
961953
MINCIOFreeTmpDimHandle(minc_dimensions, m_MINCPImpl->m_MincApparentDims);
962-
itkExceptionStringMacro("Unfortunately, only up to 3D volume are supported now.");
954+
itkExceptionStringMacro("Unfortunately, only up to 4D volumes (3 spatial + time) are supported.");
955+
}
956+
957+
if (nDims > 3) // the 4th ITK dimension is written as a MINC time dimension (slowest varying)
958+
{
959+
micreate_dimension(MItime,
960+
MI_DIMCLASS_TIME,
961+
MI_DIMATTR_REGULARLY_SAMPLED,
962+
this->GetDimensions(3),
963+
&m_MINCPImpl->m_MincApparentDims[m_MINCPImpl->m_NDims - minc_dimensions - 1]);
964+
++minc_dimensions;
963965
}
964966

965967
// allocating dimensions
@@ -994,21 +996,18 @@ MINCImageIO::WriteImageInformation()
994996
for (unsigned int i = 0; i < nDims; ++i)
995997
{
996998
const unsigned int j = i + (nComp > 1 ? 1 : 0);
997-
double dir_cos[3];
998-
for (unsigned int k = 0; k < 3; ++k)
999+
const unsigned int apparentIndex = minc_dimensions - j - 1;
1000+
miset_dimension_separation(m_MINCPImpl->m_MincApparentDims[apparentIndex], this->GetSpacing(i));
1001+
miset_dimension_start(m_MINCPImpl->m_MincApparentDims[apparentIndex], origin[i]);
1002+
if (i < 3) // spatial dimension: also set the direction cosines (time has none)
9991003
{
1000-
if (k < nDims)
1004+
double dir_cos[3];
1005+
for (unsigned int k = 0; k < 3; ++k)
10011006
{
1002-
dir_cos[k] = directionCosineMatrix[i][k];
1003-
}
1004-
else
1005-
{
1006-
dir_cos[k] = 0.0;
1007+
dir_cos[k] = (k < nDims) ? directionCosineMatrix[i][k] : 0.0;
10071008
}
1009+
miset_dimension_cosines(m_MINCPImpl->m_MincApparentDims[apparentIndex], dir_cos);
10081010
}
1009-
miset_dimension_separation(m_MINCPImpl->m_MincApparentDims[minc_dimensions - j - 1], this->GetSpacing(i));
1010-
miset_dimension_start(m_MINCPImpl->m_MincApparentDims[minc_dimensions - j - 1], origin[i]);
1011-
miset_dimension_cosines(m_MINCPImpl->m_MincApparentDims[minc_dimensions - j - 1], dir_cos);
10121011
}
10131012

10141013
// TODO: fix this to appropriate
@@ -1107,14 +1106,14 @@ MINCImageIO::WriteImageInformation()
11071106
break;
11081107
case 't':
11091108
case 'T':
1110-
if (nComp <= 1)
1109+
if (nDims <= 3) // no time dimension present
11111110
{
11121111
itkDebugMacro("Dimension order is incorrect " << dimension_order);
11131112
dimorder_good = false;
11141113
}
11151114
else
11161115
{
1117-
j = m_MINCPImpl->m_NDims - 1;
1116+
j = 0; // time is the slowest-varying apparent dimension
11181117
}
11191118
break;
11201119
case 'x':

Modules/IO/MINC/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ itk_add_test(
129129
${ITK_TEST_OUTPUT_DIR}/dti_sample.mnc
130130
ITK_REMOVE_TEMPORARY_TEST_FILES
131131
${ITK_TEST_OUTPUT_DIR}/dti_sample.mnc
132+
${ITK_TEST_OUTPUT_DIR}/dti_sample.mnc_synthetic.mnc
132133
)
133134

134135
itk_add_test(

Modules/IO/MINC/test/itkMINCImageIOTest2.cxx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ itkMINCImageIOTest2(int argc, char * argv[])
4545
ITK_EXERCISE_BASIC_OBJECT_METHODS(mincIO1, MINCImageIO, ImageIOBase);
4646

4747

48-
constexpr unsigned int supportedDimCount{ 4 }; // includes the degenerate 0-dimensional case
48+
constexpr unsigned int supportedDimCount{ 5 }; // 3 spatial + time, plus the degenerate 0-dimensional case
4949
std::vector<unsigned long> supportedDims(supportedDimCount);
5050
std::iota(std::begin(supportedDims), std::end(supportedDims), 0);
5151
for (const auto & value : supportedDims)

Modules/IO/MINC/test/itkMINCImageIOTest_4D.cxx

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616
*
1717
*=========================================================================*/
1818

19-
#include "itkImageAlgorithm.h"
2019
#include <iostream>
2120

2221
#include "itkMINCImageIOFactory.h"
2322
#include "itkImageFileReader.h"
2423
#include "itkImageFileWriter.h"
24+
#include "itkImageRegionIterator.h"
25+
#include "itkImageRegionConstIterator.h"
26+
#include "itkMath.h"
2527
#include "itkTestingMacros.h"
2628

2729

@@ -38,7 +40,10 @@ itkMINCImageIOTest_4D(int argc, char * argv[])
3840

3941
itk::MINCImageIOFactory::RegisterOneFactory();
4042

41-
using ImageType = itk::VectorImage<float, 3>;
43+
// A MINC volume with x/y/z spatial dimensions plus a time dimension is read as
44+
// a genuine 4D itk::Image, with the time step/start mapped to the 4th
45+
// dimension spacing/origin.
46+
using ImageType = itk::Image<float, 4>;
4247

4348
using ReaderType = itk::ImageFileReader<ImageType>;
4449
using WriterType = itk::ImageFileWriter<ImageType>;
@@ -52,11 +57,72 @@ itkMINCImageIOTest_4D(int argc, char * argv[])
5257

5358
ITK_TRY_EXPECT_NO_EXCEPTION(writer->Update());
5459

55-
5660
const ImageType::ConstPointer image = reader->GetOutput();
57-
5861
image->Print(std::cout);
5962

63+
// Round-trip a synthetic 4D volume with a non-trivial time step/start and
64+
// verify the time geometry survives, together with the voxel values.
65+
const std::string synthetic = std::string(argv[2]) + "_synthetic.mnc";
66+
67+
auto synthImage = ImageType::New();
68+
constexpr float timeStep = 2.5F;
69+
constexpr float timeStart = 10.0F;
70+
71+
ImageType::RegionType region;
72+
region.SetSize({ { 3, 4, 5, 2 } });
73+
synthImage->SetRegions(region);
74+
75+
ImageType::SpacingType spacing;
76+
spacing[0] = 1.0;
77+
spacing[1] = 1.0;
78+
spacing[2] = 1.0;
79+
spacing[3] = timeStep;
80+
synthImage->SetSpacing(spacing);
81+
82+
ImageType::PointType origin;
83+
origin[0] = 0.0;
84+
origin[1] = 0.0;
85+
origin[2] = 0.0;
86+
origin[3] = timeStart;
87+
synthImage->SetOrigin(origin);
88+
89+
synthImage->Allocate();
90+
91+
{
92+
float v = 0.0F;
93+
itk::ImageRegionIterator<ImageType> it(synthImage, synthImage->GetLargestPossibleRegion());
94+
for (; !it.IsAtEnd(); ++it, ++v)
95+
{
96+
it.Set(v);
97+
}
98+
}
99+
100+
auto synthWriter = WriterType::New();
101+
synthWriter->SetFileName(synthetic);
102+
synthWriter->SetInput(synthImage);
103+
ITK_TRY_EXPECT_NO_EXCEPTION(synthWriter->Update());
104+
105+
auto synthReader = ReaderType::New();
106+
synthReader->SetFileName(synthetic);
107+
ITK_TRY_EXPECT_NO_EXCEPTION(synthReader->Update());
108+
109+
const ImageType::ConstPointer synthBack = synthReader->GetOutput();
110+
111+
ITK_TEST_EXPECT_EQUAL(synthBack->GetLargestPossibleRegion().GetSize()[3], 2);
112+
ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(static_cast<float>(synthBack->GetSpacing()[3]), timeStep));
113+
ITK_TEST_EXPECT_TRUE(itk::Math::FloatAlmostEqual(static_cast<float>(synthBack->GetOrigin()[3]), timeStart));
114+
115+
itk::ImageRegionConstIterator<ImageType> inIt(synthImage, synthImage->GetLargestPossibleRegion());
116+
itk::ImageRegionConstIterator<ImageType> outIt(synthBack, synthBack->GetLargestPossibleRegion());
117+
for (; !inIt.IsAtEnd(); ++inIt, ++outIt)
118+
{
119+
if (itk::Math::NotAlmostEquals(inIt.Get(), outIt.Get()))
120+
{
121+
std::cerr << "Voxel mismatch after 4D round-trip at " << inIt.GetIndex() << ": " << inIt.Get()
122+
<< " != " << outIt.Get() << std::endl;
123+
return EXIT_FAILURE;
124+
}
125+
}
60126

61127
std::cout << "Test finished." << std::endl;
62128
return EXIT_SUCCESS;

0 commit comments

Comments
 (0)