Medical Imaging Interaction Toolkit  2016.11.0
Medical Imaging Interaction Toolkit
mitkEquiDistantBlocksSorter.cpp
Go to the documentation of this file.
1 /*=================================================================== The Medical Imaging Interaction Toolkit (MITK)
2 
3 Copyright (c) German Cancer Research Center,
4 Division of Medical and Biological Informatics.
5 All rights reserved.
6 
7 This software is distributed WITHOUT ANY WARRANTY; without
8 even the implied warranty of MERCHANTABILITY or FITNESS FOR
9 A PARTICULAR PURPOSE.
10 
11 See LICENSE.txt or http://www.mitk.org for details.
12 
13 ===================================================================*/
14 
15 //#define MBILOG_ENABLE_DEBUG
16 
18 
21 {
22 }
23 
27 {
28  return m_GroupedFiles;
29 }
30 
34 {
35  return m_UnsortedFiles;
36 }
37 
38 bool
41 {
42  return m_TiltInfo.IsRegularGantryTilt();
43 }
44 
45 void
48 {
49  m_GroupedFiles.push_back( dataset );
50 }
51 
52 void
55 {
56  m_UnsortedFiles.push_back( dataset );
57 }
58 
59 void
62 {
63  m_UnsortedFiles.insert( m_UnsortedFiles.end(), datasets.begin(), datasets.end() );
64 }
65 
66 void
69 {
70  m_FirstFilenameOfBlock = filename;
71 }
72 
73 std::string
76 {
77  return m_FirstFilenameOfBlock;
78 }
79 
80 void
83 {
84  m_LastFilenameOfBlock = filename;
85 }
86 
87 std::string
90 {
91  return m_LastFilenameOfBlock;
92 }
93 
94 
95 void
98 {
99  m_TiltInfo = tiltInfo;
100 }
101 
105 {
106  return m_TiltInfo;
107 }
108 
109 void
112 {
113  assert( !m_GroupedFiles.empty() );
114  m_UnsortedFiles.insert( m_UnsortedFiles.begin(), m_GroupedFiles.back() );
115  m_GroupedFiles.pop_back();
116  m_TiltInfo = GantryTiltInformation();
117 }
118 
119 // ------------------------ end helper class
120 
124 ,m_AcceptTilt(false)
125 ,m_ToleratedOriginOffset(0.3)
126 ,m_ToleratedOriginOffsetIsAbsolute(false)
127 ,m_AcceptTwoSlicesGroups(true)
128 {
129 }
130 
133 :DICOMDatasetSorter(other)
134 ,m_AcceptTilt(other.m_AcceptTilt)
135 ,m_ToleratedOriginOffset(other.m_ToleratedOriginOffset)
136 ,m_ToleratedOriginOffsetIsAbsolute(other.m_ToleratedOriginOffsetIsAbsolute)
137 ,m_AcceptTwoSlicesGroups(other.m_AcceptTwoSlicesGroups)
138 {
139 }
140 
143 {
144 }
145 
146 bool
148 ::operator==(const DICOMDatasetSorter& other) const
149 {
150  if (const EquiDistantBlocksSorter* otherSelf = dynamic_cast<const EquiDistantBlocksSorter*>(&other))
151  {
152  return this->m_AcceptTilt == otherSelf->m_AcceptTilt
153  && this->m_ToleratedOriginOffsetIsAbsolute == otherSelf->m_ToleratedOriginOffsetIsAbsolute
154  && this->m_AcceptTwoSlicesGroups == otherSelf->m_AcceptTwoSlicesGroups
155  && (fabs(this->m_ToleratedOriginOffset - otherSelf->m_ToleratedOriginOffset) < eps);
156  }
157  else
158  {
159  return false;
160  }
161 }
162 
163 void
165 ::PrintConfiguration(std::ostream& os, const std::string& indent) const
166 {
167  std::stringstream ts;
168  if (!m_ToleratedOriginOffsetIsAbsolute)
169  {
170  ts << "adaptive";
171  }
172  else
173  {
174  ts << m_ToleratedOriginOffset << "mm";
175  }
176 
177  os << indent << "Sort into blocks of equidistant, well-aligned (tolerance "
178  << ts.str() << ") slices "
179  << (m_AcceptTilt ? "(accepting a gantry tilt)" : "")
180  << std::endl;
181 }
182 
183 
184 void
186 ::SetAcceptTilt(bool accept)
187 {
188  m_AcceptTilt = accept;
189 }
190 
191 
192 bool
195 {
196  return m_AcceptTilt;
197 }
198 
199 void
202 {
203  m_AcceptTwoSlicesGroups = accept;
204 }
205 
206 bool
209 {
210  return m_AcceptTwoSlicesGroups;
211 }
212 
213 
217 {
218  if (this != &other)
219  {
221  m_AcceptTilt = other.m_AcceptTilt;
222  m_ToleratedOriginOffset = other.m_ToleratedOriginOffset;
223  m_ToleratedOriginOffsetIsAbsolute = other.m_ToleratedOriginOffsetIsAbsolute;
224  m_AcceptTwoSlicesGroups = other.m_AcceptTwoSlicesGroups;
225  }
226  return *this;
227 }
228 
232 {
233  DICOMTagList tags;
234  tags.push_back( DICOMTag(0x0020, 0x0032) ); // ImagePositionPatient
235  tags.push_back( DICOMTag(0x0020, 0x0037) ); // ImageOrientationPatient
236  tags.push_back( DICOMTag(0x0018, 0x1120) ); // GantryDetectorTilt
237 
238  return tags;
239 }
240 
241 void
244 {
245  DICOMDatasetList remainingInput = GetInput(); // copy
246 
247  typedef std::list<DICOMDatasetList> OutputListType;
248  OutputListType outputs;
249 
250  m_SliceGroupingResults.clear();
251 
252  while (!remainingInput.empty()) // repeat until all files are grouped somehow
253  {
254  SliceGroupingAnalysisResult regularBlock = this->AnalyzeFileForITKImageSeriesReaderSpacingAssumption( remainingInput, m_AcceptTilt );
255 
256 #ifdef MBILOG_ENABLE_DEBUG
257  DICOMDatasetList inBlock = regularBlock.GetBlockDatasets();
258  DICOMDatasetList laterBlock = regularBlock.GetUnsortedDatasets();
259  MITK_DEBUG << "Result: sorted 3D group with " << inBlock.size() << " files";
260  for (DICOMDatasetList::const_iterator diter = inBlock.cbegin(); diter != inBlock.cend(); ++diter)
261  MITK_DEBUG << " IN " << (*diter)->GetFilenameIfAvailable();
262  for (DICOMDatasetList::const_iterator diter = laterBlock.cbegin(); diter != laterBlock.cend(); ++diter)
263  MITK_DEBUG << " OUT " << (*diter)->GetFilenameIfAvailable();
264 #endif // MBILOG_ENABLE_DEBUG
265 
266  outputs.push_back( regularBlock.GetBlockDatasets() );
267  m_SliceGroupingResults.push_back( regularBlock );
268  remainingInput = regularBlock.GetUnsortedDatasets();
269  }
270 
271  unsigned int numberOfOutputs = outputs.size();
272  this->SetNumberOfOutputs(numberOfOutputs);
273 
274  unsigned int outputIndex(0);
275  for (auto oIter = outputs.cbegin();
276  oIter != outputs.cend();
277  ++outputIndex, ++oIter)
278  {
279  this->SetOutput(outputIndex, *oIter);
280  }
281 }
282 
283 void
285 ::SetToleratedOriginOffsetToAdaptive(double fractionOfInterSliceDistance)
286 {
287  m_ToleratedOriginOffset = fractionOfInterSliceDistance;
288  m_ToleratedOriginOffsetIsAbsolute = false;
289 
290  if (m_ToleratedOriginOffset < 0.0)
291  {
292  MITK_WARN << "Call SetToleratedOriginOffsetToAdaptive() only with positive numbers between 0.0 and 1.0, read documentation!";
293  }
294 
295  if (m_ToleratedOriginOffset > 0.5)
296  {
297  MITK_WARN << "EquiDistantBlocksSorter is now accepting large errors, take care of measurements, they could appear at unprecise locations!";
298  }
299 }
300 
301 void
303 ::SetToleratedOriginOffset(double millimeters)
304 {
305  m_ToleratedOriginOffset = millimeters;
306  m_ToleratedOriginOffsetIsAbsolute = true;
307  if (m_ToleratedOriginOffset < 0.0)
308  {
309  MITK_WARN << "Negative tolerance set to SetToleratedOriginOffset()!";
310  }
311 }
312 
313 double
316 {
317  return m_ToleratedOriginOffset;
318 }
319 
320 bool
323 {
324  return m_ToleratedOriginOffsetIsAbsolute;
325 }
326 
327 
328 std::string
331 {
332  return s ? std::string(s) : std::string();
333 }
334 
338  const DICOMDatasetList& datasets,
339  bool groupImagesWithGantryTilt)
340 {
341  // result.first = files that fit ITK's assumption
342  // result.second = files that do not fit, should be run through AnalyzeFileForITKImageSeriesReaderSpacingAssumption() again
344 
345  // we const_cast here, because I could not use a map.at(), which would make the code much more readable
346  const DICOMTag tagImagePositionPatient = DICOMTag(0x0020,0x0032); // Image Position (Patient)
347  const DICOMTag tagImageOrientation = DICOMTag(0x0020, 0x0037); // Image Orientation
348 
349  Vector3D fromFirstToSecondOrigin; fromFirstToSecondOrigin.Fill(0.0);
350  bool fromFirstToSecondOriginInitialized(false);
351  Point3D thisOrigin;
352  thisOrigin.Fill(0.0f);
353  Point3D lastOrigin;
354  lastOrigin.Fill(0.0f);
355  Point3D lastDifferentOrigin;
356  lastDifferentOrigin.Fill(0.0f);
357 
358  bool lastOriginInitialized(false);
359 
360  MITK_DEBUG << "--------------------------------------------------------------------------------";
361  MITK_DEBUG << "Analyzing " << datasets.size() << " files for z-spacing assumption of ITK's ImageSeriesReader (group tilted: " << groupImagesWithGantryTilt << ")";
362  unsigned int fileIndex(0);
363  double toleratedOriginError(0.005); // default: max. 1/10mm error when measurement crosses 20 slices in z direction (too strict? we don't know better)
364  for (auto dsIter = datasets.cbegin();
365  dsIter != datasets.cend();
366  ++dsIter, ++fileIndex)
367  {
368  bool fileFitsIntoPattern(false);
369  std::string thisOriginString;
370  // Read tag value into point3D. PLEASE replace this by appropriate GDCM code if you figure out how to do that
371  thisOriginString = (*dsIter)->GetTagValueAsString(tagImagePositionPatient).value;
372 
373  if (thisOriginString.empty())
374  {
375  // don't let such files be in a common group. Everything without position information will be loaded as a single slice:
376  // with standard DICOM files this can happen to: CR, DX, SC
377  MITK_DEBUG << " ==> Sort away " << *dsIter << " for later analysis (no position information)"; // we already have one occupying this position
378 
379  if ( result.GetBlockDatasets().empty() ) // nothing WITH position information yet
380  {
381  // ==> this is a group of its own, stop processing, come back later
382  result.AddFileToSortedBlock( *dsIter );
383 
384  DICOMDatasetList remainingFiles;
385  remainingFiles.insert( remainingFiles.end(), dsIter+1, datasets.end() );
386  result.AddFilesToUnsortedBlock( remainingFiles );
387 
388  fileFitsIntoPattern = false;
389  break; // no files anymore
390  }
391  else
392  {
393  // ==> this does not match, consider later
394  result.AddFileToUnsortedBlock( *dsIter ); // sort away for further analysis
395  fileFitsIntoPattern = false;
396  continue; // next file
397  }
398  }
399 
400  bool ignoredConversionError(-42); // hard to get here, no graceful way to react
401  thisOrigin = DICOMStringToPoint3D( thisOriginString, ignoredConversionError );
402 
403  MITK_DEBUG << " " << fileIndex << " " << (*dsIter)->GetFilenameIfAvailable()
404  << " at "
405  /* << thisOriginString */ << "(" << thisOrigin[0] << "," << thisOrigin[1] << "," << thisOrigin[2] << ")";
406 
407  if ( lastOriginInitialized && (thisOrigin == lastOrigin) )
408  {
409  MITK_DEBUG << " ==> Sort away " << *dsIter << " for separate time step"; // we already have one occupying this position
410  result.AddFileToUnsortedBlock( *dsIter ); // sort away for further analysis
411  fileFitsIntoPattern = false;
412  }
413  else
414  {
415  if (!fromFirstToSecondOriginInitialized && lastOriginInitialized) // calculate vector as soon as possible when we get a new position
416  {
417  fromFirstToSecondOrigin = thisOrigin - lastDifferentOrigin;
418  fromFirstToSecondOriginInitialized = true;
419 
420  // classic mode without tolerance!
421  if (!m_ToleratedOriginOffsetIsAbsolute)
422  {
423  MITK_DEBUG << "Distance of two slices: " << fromFirstToSecondOrigin.GetNorm() << "mm";
424  toleratedOriginError =
425  fromFirstToSecondOrigin.GetNorm() * 0.3; // a third of the slice distance
426  // (less than half, which would mean that a slice is displayed where another slice should actually be)
427  }
428  else
429  {
430  toleratedOriginError = m_ToleratedOriginOffset;
431  }
432  MITK_DEBUG << "Accepting errors in actual versus expected origin up to " << toleratedOriginError << "mm";
433 
434  // Here we calculate if this slice and the previous one are well aligned,
435  // i.e. we test if the previous origin is on a line through the current
436  // origin, directed into the normal direction of the current slice.
437 
438  // If this is NOT the case, then we have a data set with a TILTED GANTRY geometry,
439  // which cannot be simply loaded into a single mitk::Image at the moment.
440  // For this case, we flag this finding in the result and DicomSeriesReader
441  // can correct for that later.
442 
443  Vector3D right; right.Fill(0.0);
444  Vector3D up; right.Fill(0.0); // might be down as well, but it is just a name at this point
445  std::string orientationValue = (*dsIter)->GetTagValueAsString( tagImageOrientation ).value;
446  DICOMStringToOrientationVectors( orientationValue, right, up, ignoredConversionError );
447 
448  GantryTiltInformation tiltInfo( lastDifferentOrigin, thisOrigin, right, up, 1 );
449 
450  if ( tiltInfo.IsSheared() )
451  {
452  /* optimistic approach, accepting gantry tilt: save file for later, check all further files */
453 
454  // at this point we have TWO slices analyzed! if they are the only two files, we still split, because there is no third to verify our tilting assumption.
455  // later with a third being available, we must check if the initial tilting vector is still valid. if yes, continue.
456  // if NO, we need to split the already sorted part (result.first) and the currently analyzed file (*dsIter)
457 
458  // tell apart gantry tilt from overall skewedness
459  // sort out irregularly sheared slices, that IS NOT tilting
460 
461  if ( groupImagesWithGantryTilt && tiltInfo.IsRegularGantryTilt() )
462  {
463  assert(!datasets.empty());
464 
465  result.FlagGantryTilt(tiltInfo);
466  result.AddFileToSortedBlock( *dsIter ); // this file is good for current block
467  result.SetFirstFilenameOfBlock( datasets.front()->GetFilenameIfAvailable() );
468  result.SetLastFilenameOfBlock( datasets.back()->GetFilenameIfAvailable() );
469  fileFitsIntoPattern = true;
470  }
471  else // caller does not want tilt compensation OR shearing is more complicated than tilt
472  {
473  result.AddFileToUnsortedBlock( *dsIter ); // sort away for further analysis
474  fileFitsIntoPattern = false;
475  }
476  }
477  else // not sheared
478  {
479  result.AddFileToSortedBlock( *dsIter ); // this file is good for current block
480  fileFitsIntoPattern = true;
481  }
482  }
483  else if (fromFirstToSecondOriginInitialized) // we already know the offset between slices
484  {
485  Point3D assumedOrigin = lastDifferentOrigin + fromFirstToSecondOrigin;
486 
487  Vector3D originError = assumedOrigin - thisOrigin;
488  double norm = originError.GetNorm();
489 
490  if (norm > toleratedOriginError)
491  {
492  MITK_DEBUG << " File does not fit into the inter-slice distance pattern (diff = "
493  << norm << ", allowed "
494  << toleratedOriginError << ").";
495  MITK_DEBUG << " Expected position (" << assumedOrigin[0] << ","
496  << assumedOrigin[1] << ","
497  << assumedOrigin[2] << "), got position ("
498  << thisOrigin[0] << ","
499  << thisOrigin[1] << ","
500  << thisOrigin[2] << ")";
501  MITK_DEBUG << " ==> Sort away " << *dsIter << " for later analysis";
502 
503  // At this point we know we deviated from the expectation of ITK's ImageSeriesReader
504  // We split the input file list at this point, i.e. all files up to this one (excluding it)
505  // are returned as group 1, the remaining files (including the faulty one) are group 2
506 
507  /* Optimistic approach: check if any of the remaining slices fits in */
508  result.AddFileToUnsortedBlock( *dsIter ); // sort away for further analysis
509  fileFitsIntoPattern = false;
510  }
511  else
512  {
513  result.AddFileToSortedBlock( *dsIter ); // this file is good for current block
514  fileFitsIntoPattern = true;
515  }
516  }
517  else // this should be the very first slice
518  {
519  result.AddFileToSortedBlock( *dsIter ); // this file is good for current block
520  fileFitsIntoPattern = true;
521  }
522  }
523 
524  // record current origin for reference in later iterations
525  if ( !lastOriginInitialized || ( fileFitsIntoPattern && (thisOrigin != lastOrigin) ) )
526  {
527  lastDifferentOrigin = thisOrigin;
528  }
529 
530  lastOrigin = thisOrigin;
531  lastOriginInitialized = true;
532  }
533 
534  if ( result.ContainsGantryTilt() )
535  {
536  // check here how many files were grouped.
537  // IF it was only two files AND we assume tiltedness (e.g. save "distance")
538  // THEN we would want to also split the two previous files (simple) because
539  // we don't have any reason to assume they belong together
540 
541  // Above behavior can be configured via m_AcceptTwoSlicesGroups, the default being "do accept"
542  if ( result.GetBlockDatasets().size() == 2 && !m_AcceptTwoSlicesGroups )
543  {
544  result.UndoPrematureGrouping();
545  }
546  }
547 
548  // update tilt info to get maximum precision
549  // earlier, tilt was only calculated from first and second slice.
550  // now that we know the whole range, we can re-calculate using the very first and last slice
551  if ( result.ContainsGantryTilt() && result.GetBlockDatasets().size() > 1 )
552  {
553  try
554  {
555  DICOMDatasetList datasets = result.GetBlockDatasets();
556  DICOMDatasetAccess* firstDataset = datasets.front();
557  DICOMDatasetAccess* lastDataset = datasets.back();
558  unsigned int numberOfSlicesApart = datasets.size() - 1;
559 
560  std::string orientationString = firstDataset->GetTagValueAsString( tagImageOrientation ).value;
561  std::string firstOriginString = firstDataset->GetTagValueAsString( tagImagePositionPatient ).value;
562  std::string lastOriginString = lastDataset->GetTagValueAsString( tagImagePositionPatient ).value;
563 
564  result.FlagGantryTilt( GantryTiltInformation::MakeFromTagValues( firstOriginString, lastOriginString, orientationString, numberOfSlicesApart ));
565  }
566  catch (...)
567  {
568  // just do not flag anything, we are ok
569  }
570  }
571 
572  return result;
573 }
SliceGroupingAnalysisResult AnalyzeFileForITKImageSeriesReaderSpacingAssumption(const DICOMDatasetList &files, bool groupsOfSimilarImages)
Ensure an equal z-spacing for a group of files.
bool ContainsGantryTilt()
Wheter or not the grouped result contain a gantry tilt.
EquiDistantBlocksSorter & operator=(const EquiDistantBlocksSorter &other)
void SetToleratedOriginOffsetToAdaptive(double fractionOfInterSliceDistanct=0.3)
See class description and SetToleratedOriginOffset().
DICOMDatasetList GetUnsortedDatasets()
Remaining files, which could not be grouped.
The sorting/splitting building-block of DICOMITKSeriesGDCMReader.
bool IsRegularGantryTilt() const
Whether the shearing is a gantry tilt or more complicated.
virtual void Sort() override
Delegates work to AnalyzeFileForITKImageSeriesReaderSpacingAssumption(). AnalyzeFileForITKImageSeries...
void SetAcceptTilt(bool accept)
Whether or not to accept images from a tilted acquisition in a single output group.
Split inputs into blocks of equidistant slices (for use in DICOMITKSeriesGDCMReader).
std::vector< DICOMTag > DICOMTagList
Definition: mitkDICOMTag.h:64
void DICOMStringToOrientationVectors(const std::string &s, Vector3D &right, Vector3D &up, bool &successful)
Convert DICOM string describing a point two Vector3D.
Representation of a DICOM tag.
Definition: mitkDICOMTag.h:37
#define MITK_DEBUG
Definition: mitkLogMacros.h:26
virtual DICOMTagList GetTagsOfInterest() override
std::string ConstCharStarToString(const char *s)
Safely convert const char* to std::string.
Interface to datasets that is presented to sorting classes such as DICOMDatasetSorter.
Return type of AnalyzeFileForITKImageSeriesReaderSpacingAssumption().
DICOMDatasetList GetBlockDatasets()
Grouping result, all same origin-to-origin distance w/o gaps.
#define MITK_WARN
Definition: mitkLogMacros.h:23
virtual DICOMDatasetFinding GetTagValueAsString(const DICOMTag &tag) const =0
Return a DICOMDatasetFinding instance of the tag. The return containes (if valid) the raw value of th...
void AddFileToSortedBlock(DICOMDatasetAccess *dataset)
Meant for internal use by AnalyzeFileForITKImageSeriesReaderSpacingAssumption only.
static const std::string filename
const GantryTiltInformation & GetTiltInfo() const
Detailed description of gantry tilt.
bool IsSheared() const
Whether the slices were sheared.
static GantryTiltInformation MakeFromTagValues(const std::string &origin1String, const std::string &origin2String, const std::string orientationString, unsigned int numberOfSlicesApart)
Factory method to create GantryTiltInformation from tag values (strings).
Gantry tilt analysis result.
MITKCORE_EXPORT const ScalarType eps
Point3D DICOMStringToPoint3D(const std::string &s, bool &successful)
Convert DICOM string describing a point to Point3D.
DICOMDatasetSorter & operator=(const DICOMDatasetSorter &other)
void UndoPrematureGrouping()
Only meaningful for use by AnalyzeFileForITKImageSeriesReaderSpacingAssumption.
std::vector< DICOMDatasetAccess * > DICOMDatasetList
virtual bool operator==(const DICOMDatasetSorter &other) const override
void SetToleratedOriginOffset(double millimeters=0.005)
See class description and SetToleratedOriginOffsetToAdaptive().
virtual void PrintConfiguration(std::ostream &os, const std::string &indent="") const override
Print configuration details into stream.
void FlagGantryTilt(const GantryTiltInformation &tiltInfo)
Meant for internal use by AnalyzeFileForITKImageSeriesReaderSpacingAssumption only.
void AddFileToUnsortedBlock(DICOMDatasetAccess *dataset)
Meant for internal use by AnalyzeFileForITKImageSeriesReaderSpacingAssumption only.