1 /*============================================================================
3 The Medical Imaging Interaction Toolkit (MITK)
5 Copyright (c) German Cancer Research Center (DKFZ)
8 Use of this source code is governed by a 3-clause BSD license that can be
9 found in the LICENSE file.
11 ============================================================================*/
13 #include "mitkITKDICOMSeriesReaderHelper.h"
15 #include <itkImageSeriesReader.h>
16 #include <itkResampleImageFilter.h>
17 //#include <itkAffineTransform.h>
18 //#include <itkLinearInterpolateImageFunction.h>
19 //#include <itkTimeProbesCollectorBase.h>
21 #include "dcmtk/ofstd/ofdatime.h"
23 template <typename PixelType>
25 mitk::ITKDICOMSeriesReaderHelper
27 const StringContainer& filenames,
29 const GantryTiltInformation& tiltInfo,
30 itk::GDCMImageIO::Pointer& io)
32 /******** Normal Case, 3D (also for GDCM < 2 usable) ***************/
33 mitk::Image::Pointer image = mitk::Image::New();
35 typedef itk::Image<PixelType, 3> ImageType;
36 typedef itk::ImageSeriesReader<ImageType> ReaderType;
38 io = itk::GDCMImageIO::New();
39 typename ReaderType::Pointer reader = ReaderType::New();
41 reader->SetImageIO(io);
42 reader->ReverseOrderOff(); // at this point we require an order of input images so that
43 // the direction between the origin of the first and the last slice
44 // is the same direction as the image normals! Otherwise we might
45 // see images upside down. Unclear whether this is a bug in MITK,
46 // see NormalDirectionConsistencySorter.
48 reader->SetFileNames(filenames);
50 typename ImageType::Pointer readVolume = reader->GetOutput();
52 // if we detected that the images are from a tilted gantry acquisition, we need to push some pixels into the right position
55 readVolume = FixUpTiltedGeometry( reader->GetOutput(), tiltInfo );
58 image->InitializeByItk(readVolume.GetPointer());
59 image->SetImportVolume(readVolume->GetBufferPointer());
61 #ifdef MBILOG_ENABLE_DEBUG
63 MITK_DEBUG << "Volume dimension: [" << image->GetDimension(0) << ", "
64 << image->GetDimension(1) << ", "
65 << image->GetDimension(2) << "]";
67 MITK_DEBUG << "Volume spacing: [" << image->GetGeometry()->GetSpacing()[0] << ", "
68 << image->GetGeometry()->GetSpacing()[1] << ", "
69 << image->GetGeometry()->GetSpacing()[2] << "]";
70 #endif //MBILOG_ENABLE_DEBUG
75 #define MITK_DEBUG_OUTPUT_FILELIST(list)\
76 MITK_DEBUG << "-------------------------------------------"; \
77 for (StringContainer::const_iterator _iter = (list).cbegin(); _iter!=(list).cend(); ++_iter) \
79 MITK_DEBUG <<" file '" << *_iter<< "'"; \
81 MITK_DEBUG << "-------------------------------------------";
83 template <typename PixelType>
85 mitk::ITKDICOMSeriesReaderHelper
87 const StringContainerList& filenamesForTimeSteps,
89 const GantryTiltInformation& tiltInfo,
90 itk::GDCMImageIO::Pointer& io)
92 unsigned int numberOfTimeSteps = filenamesForTimeSteps.size();
94 MITK_DEBUG << "Start extracting time bounds of time steps";
95 const TimeBoundsList timeBoundsList = ExtractTimeBoundsOfTimeSteps(filenamesForTimeSteps);
96 if (numberOfTimeSteps!=timeBoundsList.size())
98 mitkThrow() << "Error while loading 3D+t. Inconsistent size of generated time bounds list. List size: "<< timeBoundsList.size() << "; number of steps: "<<numberOfTimeSteps;
101 mitk::Image::Pointer image = mitk::Image::New();
103 typedef itk::Image<PixelType, 4> ImageType;
104 typedef itk::ImageSeriesReader<ImageType> ReaderType;
106 io = itk::GDCMImageIO::New();
107 typename ReaderType::Pointer reader = ReaderType::New();
109 reader->SetImageIO(io);
110 reader->ReverseOrderOff(); // at this point we require an order of input images so that
111 // the direction between the origin of the first and the last slice
112 // is the same direction as the image normals! Otherwise we might
113 // see images upside down. Unclear whether this is a bug in MITK,
114 // see NormalDirectionConsistencySorter.
117 unsigned int currentTimeStep = 0;
119 #ifdef MBILOG_ENABLE_DEBUG
120 MITK_DEBUG << "Start loading timestep " << currentTimeStep;
121 MITK_DEBUG_OUTPUT_FILELIST( filenamesForTimeSteps.front() )
122 #endif // MBILOG_ENABLE_DEBUG
124 reader->SetFileNames(filenamesForTimeSteps.front());
126 typename ImageType::Pointer readVolume = reader->GetOutput();
128 // if we detected that the images are from a tilted gantry acquisition, we need to push some pixels into the right position
131 readVolume = FixUpTiltedGeometry( reader->GetOutput(), tiltInfo );
134 image->InitializeByItk(readVolume.GetPointer(), 1, numberOfTimeSteps);
135 image->SetImportVolume(readVolume->GetBufferPointer(), currentTimeStep++); // timestep 0
137 // for other time-steps
138 for (auto timestepsIter = ++(filenamesForTimeSteps.cbegin()); // start with SECOND entry
139 timestepsIter != filenamesForTimeSteps.cend();
140 ++currentTimeStep, ++timestepsIter)
142 #ifdef MBILOG_ENABLE_DEBUG
143 MITK_DEBUG << "Start loading timestep " << currentTimeStep;
144 MITK_DEBUG_OUTPUT_FILELIST( *timestepsIter )
145 #endif // MBILOG_ENABLE_DEBUG
147 reader->SetFileNames( *timestepsIter );
149 readVolume = reader->GetOutput();
153 readVolume = FixUpTiltedGeometry( reader->GetOutput(), tiltInfo );
156 image->SetImportVolume(readVolume->GetBufferPointer(), currentTimeStep);
159 #ifdef MBILOG_ENABLE_DEBUG
160 MITK_DEBUG << "Volume dimension: [" << image->GetDimension(0) << ", "
161 << image->GetDimension(1) << ", "
162 << image->GetDimension(2) << "]";
164 MITK_DEBUG << "Volume spacing: [" << image->GetGeometry()->GetSpacing()[0] << ", "
165 << image->GetGeometry()->GetSpacing()[1] << ", "
166 << image->GetGeometry()->GetSpacing()[2] << "]";
167 #endif // MBILOG_ENABLE_DEBUG
169 //construct timegeometry
170 TimeGeometry::Pointer timeGeometry = GenerateTimeGeometry(image->GetGeometry(),timeBoundsList);
171 image->SetTimeGeometry(timeGeometry);
176 template <typename ImageType>
177 typename ImageType::Pointer
178 mitk::ITKDICOMSeriesReaderHelper
179 ::FixUpTiltedGeometry( ImageType* input, const GantryTiltInformation& tiltInfo )
181 typedef itk::ResampleImageFilter<ImageType,ImageType> ResampleFilterType;
182 typename ResampleFilterType::Pointer resampler = ResampleFilterType::New();
183 resampler->SetInput( input );
186 Transform for a point is
187 - transform from actual position to index coordinates
188 - apply a shear that undoes the gantry tilt
189 - transform back into world coordinates
191 Anybody who does this in a simpler way: don't forget to write up how and why your solution works
193 typedef itk::ScalableAffineTransform< double, ImageType::ImageDimension > TransformType;
194 typename TransformType::Pointer transformShear = TransformType::New();
197 - apply a shear and spacing correction to the image block that corrects the ITK reader's error
198 - ITK ignores the shear and loads slices into an orthogonal volume
199 - ITK calculates the spacing from the origin distance, which is more than the actual spacing with gantry tilt images
201 - we have calculated some information in tiltInfo:
202 - the shift in Y direction that is added with each additional slice is the most important information
203 - the Y-shift is calculated in mm world coordinates
204 - we apply a shearing transformation to the ITK-read image volume
205 - to do this locally,
206 - we transform the image volume back to origin and "normal" orientation by applying the inverse of its transform
207 (this brings us into the image's "index coordinate" system)
208 - we apply a shear with the Y-shift factor put into a unit transform at row 1, col 2
209 - we transform the image volume back to its actual position (from index to world coordinates)
210 - we lastly apply modify the image spacing in z direction by replacing this number with the correctly calulcated inter-slice distance
213 const ScalarType factor = tiltInfo.GetMatrixCoefficientForCorrectionInWorldCoordinates() / input->GetSpacing()[1];
214 // row 1, column 2 corrects shear in parallel to Y axis, proportional to distance in Z direction
215 transformShear->Shear( 1, 2, factor );
217 typename TransformType::Pointer imageIndexToWorld = TransformType::New();
218 imageIndexToWorld->SetOffset( input->GetOrigin().GetVectorFromOrigin() );
220 typename TransformType::MatrixType indexToWorldMatrix;
221 indexToWorldMatrix = input->GetDirection();
223 typename ImageType::DirectionType scale;
224 for ( unsigned int i = 0; i < ImageType::ImageDimension; i++ )
226 scale[i][i] = input->GetSpacing()[i];
228 indexToWorldMatrix *= scale;
230 imageIndexToWorld->SetMatrix( indexToWorldMatrix );
232 typename TransformType::Pointer imageWorldToIndex = TransformType::New();
233 imageIndexToWorld->GetInverse( imageWorldToIndex );
235 typename TransformType::Pointer gantryTiltCorrection = TransformType::New();
236 gantryTiltCorrection->Compose( imageWorldToIndex );
237 gantryTiltCorrection->Compose( transformShear );
238 gantryTiltCorrection->Compose( imageIndexToWorld );
240 resampler->SetTransform( gantryTiltCorrection );
242 typedef itk::LinearInterpolateImageFunction< ImageType, double > InterpolatorType;
243 typename InterpolatorType::Pointer interpolator = InterpolatorType::New();
244 resampler->SetInterpolator( interpolator );
246 This would be the right place to invent a meaningful value for positions outside of the image.
247 For CT, HU -1000 might be meaningful, but a general solution seems not possible. Even for CT,
248 -1000 would only look natural for many not all images.
251 // TODO use (0028,0120) Pixel Padding Value if present
252 resampler->SetDefaultPixelValue( itk::NumericTraits< typename ImageType::PixelType >::min() );
254 // adjust size in Y direction! (maybe just transform the outer last pixel to see how much space we would need
256 resampler->SetOutputParametersFromImage( input ); // we basically need the same image again, just sheared
259 // if tilt positive, then we need additional pixels BELOW origin, otherwise we need pixels behind the end of the block
261 // in any case we need more size to accomodate shifted slices
262 typename ImageType::SizeType largerSize = resampler->GetSize(); // now the resampler already holds the input image's size.
263 const double imageSizeZ = largerSize[2];
264 //MITK_DEBUG <<"Calculate lager size = " << largerSize[1] << " + " << tiltInfo.GetTiltCorrectedAdditionalSize(imageSizeZ) << " / " << input->GetSpacing()[1] << "+ 2.0";
265 largerSize[1] += static_cast<typename ImageType::SizeType::SizeValueType>(tiltInfo.GetTiltCorrectedAdditionalSize(imageSizeZ) / input->GetSpacing()[1]+ 2.0);
266 resampler->SetSize( largerSize );
267 //MITK_DEBUG << "Fix Y size of image w/ spacing " << input->GetSpacing()[1] << " from " << input->GetLargestPossibleRegion().GetSize()[1] << " to " << largerSize[1];
269 // in SOME cases this additional size is below/behind origin
270 if ( tiltInfo.GetMatrixCoefficientForCorrectionInWorldCoordinates() > 0.0 )
272 typename ImageType::DirectionType imageDirection = input->GetDirection();
274 yDirection[0] = imageDirection[0][1];
275 yDirection[1] = imageDirection[1][1];
276 yDirection[2] = imageDirection[2][1];
277 yDirection.Normalize();
279 typename ImageType::PointType shiftedOrigin;
280 shiftedOrigin = input->GetOrigin();
282 // add some pixels to make everything fit
283 shiftedOrigin[0] -= yDirection[0] * (tiltInfo.GetTiltCorrectedAdditionalSize(imageSizeZ) + 1.0 * input->GetSpacing()[1]);
284 shiftedOrigin[1] -= yDirection[1] * (tiltInfo.GetTiltCorrectedAdditionalSize(imageSizeZ) + 1.0 * input->GetSpacing()[1]);
285 shiftedOrigin[2] -= yDirection[2] * (tiltInfo.GetTiltCorrectedAdditionalSize(imageSizeZ) + 1.0 * input->GetSpacing()[1]);
287 resampler->SetOutputOrigin( shiftedOrigin );
291 typename ImageType::Pointer result = resampler->GetOutput();
293 // ImageSeriesReader calculates z spacing as the distance between the first two origins.
294 // This is not correct in case of gantry tilt, so we set our calculated spacing.
295 typename ImageType::SpacingType correctedSpacing = result->GetSpacing();
296 correctedSpacing[2] = tiltInfo.GetRealZSpacing();
297 result->SetSpacing( correctedSpacing );