Medical Imaging Interaction Toolkit  2016.11.0
Medical Imaging Interaction Toolkit
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
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.