1 /*===================================================================
3 The Medical Imaging Interaction Toolkit (MITK)
5 Copyright (c) German Cancer Research Center,
6 Division of Medical and Biological Informatics.
9 This software is distributed WITHOUT ANY WARRANTY; without
10 even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 See LICENSE.txt or http://www.mitk.org for details.
14 ===================================================================*/
16 #include "mitkITKDICOMSeriesReaderHelper.h"
18 #include <itkImageSeriesReader.h>
19 #include <itkResampleImageFilter.h>
20 //#include <itkAffineTransform.h>
21 //#include <itkLinearInterpolateImageFunction.h>
22 //#include <itkTimeProbesCollectorBase.h>
26 template <typename PixelType>
28 mitk::ITKDICOMSeriesReaderHelper
30 const StringContainer& filenames,
32 const GantryTiltInformation& tiltInfo,
33 itk::GDCMImageIO::Pointer& io)
35 /******** Normal Case, 3D (also for GDCM < 2 usable) ***************/
36 mitk::Image::Pointer image = mitk::Image::New();
38 typedef itk::Image<PixelType, 3> ImageType;
39 typedef itk::ImageSeriesReader<ImageType> ReaderType;
41 io = itk::GDCMImageIO::New();
42 typename ReaderType::Pointer reader = ReaderType::New();
44 reader->SetImageIO(io);
45 reader->ReverseOrderOff(); // at this point we require an order of input images so that
46 // the direction between the origin of the first and the last slice
47 // is the same direction as the image normals! Otherwise we might
48 // see images upside down. Unclear whether this is a bug in MITK,
49 // see NormalDirectionConsistencySorter.
51 reader->SetFileNames(filenames);
53 typename ImageType::Pointer readVolume = reader->GetOutput();
55 // if we detected that the images are from a tilted gantry acquisition, we need to push some pixels into the right position
58 readVolume = FixUpTiltedGeometry( reader->GetOutput(), tiltInfo );
61 image->InitializeByItk(readVolume.GetPointer());
62 image->SetImportVolume(readVolume->GetBufferPointer());
64 #ifdef MBILOG_ENABLE_DEBUG
66 MITK_DEBUG << "Volume dimension: [" << image->GetDimension(0) << ", "
67 << image->GetDimension(1) << ", "
68 << image->GetDimension(2) << "]";
70 MITK_DEBUG << "Volume spacing: [" << image->GetGeometry()->GetSpacing()[0] << ", "
71 << image->GetGeometry()->GetSpacing()[1] << ", "
72 << image->GetGeometry()->GetSpacing()[2] << "]";
73 #endif //MBILOG_ENABLE_DEBUG
78 #define MITK_DEBUG_OUTPUT_FILELIST(list)\
79 MITK_DEBUG << "-------------------------------------------"; \
80 for (StringContainer::const_iterator _iter = (list).cbegin(); _iter!=(list).cend(); ++_iter) \
82 MITK_DEBUG <<" file '" << *_iter<< "'"; \
84 MITK_DEBUG << "-------------------------------------------";
86 template <typename PixelType>
88 mitk::ITKDICOMSeriesReaderHelper
90 const StringContainerList& filenamesForTimeSteps,
92 const GantryTiltInformation& tiltInfo,
93 itk::GDCMImageIO::Pointer& io)
95 unsigned int numberOfTimeSteps = filenamesForTimeSteps.size();
97 MITK_DEBUG << "Start extracting time bounds of time steps";
98 const TimeBoundsList timeBoundsList = ExtractTimeBoundsOfTimeSteps(filenamesForTimeSteps);
99 if (numberOfTimeSteps!=timeBoundsList.size())
101 mitkThrow() << "Error while loading 3D+t. Inconsistent size of generated time bounds list. List size: "<< timeBoundsList.size() << "; number of steps: "<<numberOfTimeSteps;
104 mitk::Image::Pointer image = mitk::Image::New();
106 typedef itk::Image<PixelType, 4> ImageType;
107 typedef itk::ImageSeriesReader<ImageType> ReaderType;
109 io = itk::GDCMImageIO::New();
110 typename ReaderType::Pointer reader = ReaderType::New();
112 reader->SetImageIO(io);
113 reader->ReverseOrderOff(); // at this point we require an order of input images so that
114 // the direction between the origin of the first and the last slice
115 // is the same direction as the image normals! Otherwise we might
116 // see images upside down. Unclear whether this is a bug in MITK,
117 // see NormalDirectionConsistencySorter.
120 unsigned int currentTimeStep = 0;
122 #ifdef MBILOG_ENABLE_DEBUG
123 MITK_DEBUG << "Start loading timestep " << currentTimeStep;
124 MITK_DEBUG_OUTPUT_FILELIST( filenamesForTimeSteps.front() )
125 #endif // MBILOG_ENABLE_DEBUG
127 reader->SetFileNames(filenamesForTimeSteps.front());
129 typename ImageType::Pointer readVolume = reader->GetOutput();
131 // if we detected that the images are from a tilted gantry acquisition, we need to push some pixels into the right position
134 readVolume = FixUpTiltedGeometry( reader->GetOutput(), tiltInfo );
137 image->InitializeByItk(readVolume.GetPointer(), 1, numberOfTimeSteps);
138 image->SetImportVolume(readVolume->GetBufferPointer(), currentTimeStep++); // timestep 0
140 // for other time-steps
141 for (auto timestepsIter = ++(filenamesForTimeSteps.cbegin()); // start with SECOND entry
142 timestepsIter != filenamesForTimeSteps.cend();
143 ++currentTimeStep, ++timestepsIter)
145 #ifdef MBILOG_ENABLE_DEBUG
146 MITK_DEBUG << "Start loading timestep " << currentTimeStep;
147 MITK_DEBUG_OUTPUT_FILELIST( *timestepsIter )
148 #endif // MBILOG_ENABLE_DEBUG
150 reader->SetFileNames( *timestepsIter );
152 readVolume = reader->GetOutput();
156 readVolume = FixUpTiltedGeometry( reader->GetOutput(), tiltInfo );
159 image->SetImportVolume(readVolume->GetBufferPointer(), currentTimeStep);
162 #ifdef MBILOG_ENABLE_DEBUG
163 MITK_DEBUG << "Volume dimension: [" << image->GetDimension(0) << ", "
164 << image->GetDimension(1) << ", "
165 << image->GetDimension(2) << "]";
167 MITK_DEBUG << "Volume spacing: [" << image->GetGeometry()->GetSpacing()[0] << ", "
168 << image->GetGeometry()->GetSpacing()[1] << ", "
169 << image->GetGeometry()->GetSpacing()[2] << "]";
170 #endif // MBILOG_ENABLE_DEBUG
172 //construct timegeometry
173 TimeGeometry::Pointer timeGeometry = GenerateTimeGeometry(image->GetGeometry(),timeBoundsList);
174 image->SetTimeGeometry(timeGeometry);
179 template <typename ImageType>
180 typename ImageType::Pointer
181 mitk::ITKDICOMSeriesReaderHelper
182 ::FixUpTiltedGeometry( ImageType* input, const GantryTiltInformation& tiltInfo )
184 typedef itk::ResampleImageFilter<ImageType,ImageType> ResampleFilterType;
185 typename ResampleFilterType::Pointer resampler = ResampleFilterType::New();
186 resampler->SetInput( input );
189 Transform for a point is
190 - transform from actual position to index coordinates
191 - apply a shear that undoes the gantry tilt
192 - transform back into world coordinates
194 Anybody who does this in a simpler way: don't forget to write up how and why your solution works
196 typedef itk::ScalableAffineTransform< double, ImageType::ImageDimension > TransformType;
197 typename TransformType::Pointer transformShear = TransformType::New();
200 - apply a shear and spacing correction to the image block that corrects the ITK reader's error
201 - ITK ignores the shear and loads slices into an orthogonal volume
202 - ITK calculates the spacing from the origin distance, which is more than the actual spacing with gantry tilt images
204 - we have calculated some information in tiltInfo:
205 - the shift in Y direction that is added with each additional slice is the most important information
206 - the Y-shift is calculated in mm world coordinates
207 - we apply a shearing transformation to the ITK-read image volume
208 - to do this locally,
209 - we transform the image volume back to origin and "normal" orientation by applying the inverse of its transform
210 (this brings us into the image's "index coordinate" system)
211 - we apply a shear with the Y-shift factor put into a unit transform at row 1, col 2
212 - we transform the image volume back to its actual position (from index to world coordinates)
213 - we lastly apply modify the image spacing in z direction by replacing this number with the correctly calulcated inter-slice distance
216 const ScalarType factor = tiltInfo.GetMatrixCoefficientForCorrectionInWorldCoordinates() / input->GetSpacing()[1];
217 // row 1, column 2 corrects shear in parallel to Y axis, proportional to distance in Z direction
218 transformShear->Shear( 1, 2, factor );
220 typename TransformType::Pointer imageIndexToWorld = TransformType::New();
221 imageIndexToWorld->SetOffset( input->GetOrigin().GetVectorFromOrigin() );
223 typename TransformType::MatrixType indexToWorldMatrix;
224 indexToWorldMatrix = input->GetDirection();
226 typename ImageType::DirectionType scale;
227 for ( unsigned int i = 0; i < ImageType::ImageDimension; i++ )
229 scale[i][i] = input->GetSpacing()[i];
231 indexToWorldMatrix *= scale;
233 imageIndexToWorld->SetMatrix( indexToWorldMatrix );
235 typename TransformType::Pointer imageWorldToIndex = TransformType::New();
236 imageIndexToWorld->GetInverse( imageWorldToIndex );
238 typename TransformType::Pointer gantryTiltCorrection = TransformType::New();
239 gantryTiltCorrection->Compose( imageWorldToIndex );
240 gantryTiltCorrection->Compose( transformShear );
241 gantryTiltCorrection->Compose( imageIndexToWorld );
243 resampler->SetTransform( gantryTiltCorrection );
245 typedef itk::LinearInterpolateImageFunction< ImageType, double > InterpolatorType;
246 typename InterpolatorType::Pointer interpolator = InterpolatorType::New();
247 resampler->SetInterpolator( interpolator );
249 This would be the right place to invent a meaningful value for positions outside of the image.
250 For CT, HU -1000 might be meaningful, but a general solution seems not possible. Even for CT,
251 -1000 would only look natural for many not all images.
254 // TODO use (0028,0120) Pixel Padding Value if present
255 resampler->SetDefaultPixelValue( itk::NumericTraits< typename ImageType::PixelType >::min() );
257 // adjust size in Y direction! (maybe just transform the outer last pixel to see how much space we would need
259 resampler->SetOutputParametersFromImage( input ); // we basically need the same image again, just sheared
262 // if tilt positive, then we need additional pixels BELOW origin, otherwise we need pixels behind the end of the block
264 // in any case we need more size to accomodate shifted slices
265 typename ImageType::SizeType largerSize = resampler->GetSize(); // now the resampler already holds the input image's size.
266 const double imageSizeZ = largerSize[2];
267 //MITK_DEBUG <<"Calculate lager size = " << largerSize[1] << " + " << tiltInfo.GetTiltCorrectedAdditionalSize(imageSizeZ) << " / " << input->GetSpacing()[1] << "+ 2.0";
268 largerSize[1] += static_cast<typename ImageType::SizeType::SizeValueType>(tiltInfo.GetTiltCorrectedAdditionalSize(imageSizeZ) / input->GetSpacing()[1]+ 2.0);
269 resampler->SetSize( largerSize );
270 //MITK_DEBUG << "Fix Y size of image w/ spacing " << input->GetSpacing()[1] << " from " << input->GetLargestPossibleRegion().GetSize()[1] << " to " << largerSize[1];
272 // in SOME cases this additional size is below/behind origin
273 if ( tiltInfo.GetMatrixCoefficientForCorrectionInWorldCoordinates() > 0.0 )
275 typename ImageType::DirectionType imageDirection = input->GetDirection();
277 yDirection[0] = imageDirection[0][1];
278 yDirection[1] = imageDirection[1][1];
279 yDirection[2] = imageDirection[2][1];
280 yDirection.Normalize();
282 typename ImageType::PointType shiftedOrigin;
283 shiftedOrigin = input->GetOrigin();
285 // add some pixels to make everything fit
286 shiftedOrigin[0] -= yDirection[0] * (tiltInfo.GetTiltCorrectedAdditionalSize(imageSizeZ) + 1.0 * input->GetSpacing()[1]);
287 shiftedOrigin[1] -= yDirection[1] * (tiltInfo.GetTiltCorrectedAdditionalSize(imageSizeZ) + 1.0 * input->GetSpacing()[1]);
288 shiftedOrigin[2] -= yDirection[2] * (tiltInfo.GetTiltCorrectedAdditionalSize(imageSizeZ) + 1.0 * input->GetSpacing()[1]);
290 resampler->SetOutputOrigin( shiftedOrigin );
294 typename ImageType::Pointer result = resampler->GetOutput();
296 // ImageSeriesReader calculates z spacing as the distance between the first two origins.
297 // This is not correct in case of gantry tilt, so we set our calculated spacing.
298 typename ImageType::SpacingType correctedSpacing = result->GetSpacing();
299 correctedSpacing[2] = tiltInfo.GetRealZSpacing();
300 result->SetSpacing( correctedSpacing );