changeset 1478:fab6c6e795a3

Framework/Toolbox/SortedFrames.cpp
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 18 Jun 2020 16:01:00 +0200
parents 5732edec7cbd
children 5e3cfe87a873
files Framework/Toolbox/SortedFrames.cpp Framework/Toolbox/SortedFrames.h Resources/CMake/OrthancStoneConfiguration.cmake UnitTestsSources/SortedFramesTests.cpp
diffstat 4 files changed, 567 insertions(+), 473 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Toolbox/SortedFrames.cpp	Thu Jun 18 16:01:00 2020 +0200
@@ -0,0 +1,388 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "SortedFrames.h"
+
+#include <OrthancException.h>
+
+#include "GeometryToolbox.h"
+
+namespace OrthancStone
+{
+  SortedFrames::Instance::Instance(const Orthanc::DicomMap& tags)
+  {
+    tags_.Assign(tags);
+
+    if (!tags.LookupStringValue(sopInstanceUid_, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+
+    uint32_t tmp;
+    if (tags.ParseUnsignedInteger32(tmp, Orthanc::DICOM_TAG_NUMBER_OF_FRAMES))
+    {
+      numberOfFrames_ = tmp;
+    }
+    else
+    {
+      numberOfFrames_ = 1;
+    }
+
+    hasPosition_ = (
+      LinearAlgebra::ParseVector(position_, tags, Orthanc::DICOM_TAG_IMAGE_POSITION_PATIENT) &&
+      position_.size() == 3 &&
+      GeometryToolbox::ComputeNormal(normal_, tags));
+  }
+
+
+  const Vector& SortedFrames::Instance::GetNormal() const
+  {
+    if (hasPosition_)
+    {
+      return normal_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  const Vector& SortedFrames::Instance::GetPosition() const
+  {
+    if (hasPosition_)
+    {
+      return position_;
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+  
+  SortedFrames::Frame::Frame(const Instance& instance,
+                             unsigned int frameIndex) :
+    instance_(instance),
+    frameIndex_(frameIndex)
+  {
+    if (frameIndex >= instance.GetNumberOfFrames())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const SortedFrames::Instance& SortedFrames::GetInstance(size_t index) const
+  {
+    if (index >= instances_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      assert(instances_[index] != NULL);
+      return *instances_[index];
+    }
+  }
+
+  
+  const SortedFrames::Frame& SortedFrames::GetFrame(size_t index) const
+  {
+    if (!sorted_)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls,
+                                      "Sort() has not been called");
+    }
+    if (index >= frames_.size())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return frames_[index];
+    }    
+  }
+
+  
+  SortedFrames::~SortedFrames()
+  {
+    for (size_t i = 0; i < instances_.size(); i++)
+    {
+      assert(instances_[i] != NULL);
+      delete instances_[i];
+    }
+  }
+
+
+  void SortedFrames::AddInstance(const Orthanc::DicomMap& tags)
+  {
+    std::unique_ptr<Instance> instance(new Instance(tags));
+
+    std::string studyInstanceUid, seriesInstanceUid;
+    if (!tags.LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) ||
+        !tags.LookupStringValue(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+    
+    if (instances_.empty())
+    {
+      studyInstanceUid_ = studyInstanceUid;
+      seriesInstanceUid_ = seriesInstanceUid;
+    }
+    else
+    {
+      if (studyInstanceUid_ != studyInstanceUid ||
+          seriesInstanceUid_ != seriesInstanceUid)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
+                                        "Mixing instances from different series");
+      }
+    }
+
+    instances_.push_back(instance.release());
+    sorted_ = false;
+    frames_.clear();
+  }
+
+
+  void SortedFrames::AddFramesOfInstance(std::set<size_t>& remainingInstances,
+                                         size_t index)
+  {
+    assert(instances_[index] != NULL);
+    const Instance& instance = *instances_[index];
+    
+    for (unsigned int i = 0; i < instance.GetNumberOfFrames(); i++)
+    {
+      frames_.push_back(Frame(instance, i));
+    }
+
+    assert(remainingInstances.find(index) != remainingInstances.end());
+    remainingInstances.erase(index);
+  }
+
+
+  namespace
+  {
+    template<typename T>
+    class SortableItem
+    {
+    private:
+      T            value_;
+      size_t       instance_;
+      std::string  sopInstanceUid_;
+
+    public:
+      SortableItem(const T& value,
+                   size_t instance,
+                   const std::string& sopInstanceUid) :
+        value_(value),
+        instance_(instance),
+        sopInstanceUid_(sopInstanceUid)
+      {
+      }
+
+      size_t GetInstanceIndex() const
+      {
+        return instance_;
+      }
+
+      bool operator< (const SortableItem& other) const
+      {
+        return (value_ < other.value_ ||
+                (value_ == other.value_ &&
+                 sopInstanceUid_ < other.sopInstanceUid_));
+      }
+    };
+  }
+
+
+  void SortedFrames::SortUsingIntegerTag(std::set<size_t>& remainingInstances,
+                                         const Orthanc::DicomTag& tag)
+  {
+    std::vector< SortableItem<int32_t> > items;
+    items.reserve(remainingInstances.size());
+
+    for (std::set<size_t>::const_iterator it = remainingInstances.begin();
+         it != remainingInstances.end(); ++it)
+    {
+      assert(instances_[*it] != NULL);
+      const Instance& instance = *instances_[*it];
+
+      int32_t value;
+      std::string sopInstanceUid;
+      if (instance.GetTags().ParseInteger32(value, tag) &&
+          instance.GetTags().LookupStringValue(
+            sopInstanceUid, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false))
+      {
+        items.push_back(SortableItem<int32_t>(value, *it, sopInstanceUid));
+      }
+    }
+    
+    std::sort(items.begin(), items.end());
+
+    for (size_t i = 0; i < items.size(); i++)
+    {
+      AddFramesOfInstance(remainingInstances, items[i].GetInstanceIndex());
+    }
+  }
+
+
+  void SortedFrames::SortUsingSopInstanceUid(std::set<size_t>& remainingInstances)
+  {
+    std::vector<SortableItem<int32_t> > items;
+    items.reserve(remainingInstances.size());
+
+    for (std::set<size_t>::const_iterator it = remainingInstances.begin();
+         it != remainingInstances.end(); ++it)
+    {
+      assert(instances_[*it] != NULL);
+      const Instance& instance = *instances_[*it];
+
+      std::string sopInstanceUid;
+      if (instance.GetTags().LookupStringValue(
+            sopInstanceUid, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false))
+      {
+        items.push_back(SortableItem<int32_t>(0 /* arbitrary value */, *it, sopInstanceUid));
+      }
+    }
+    
+    std::sort(items.begin(), items.end());
+
+    for (size_t i = 0; i < items.size(); i++)
+    {
+      AddFramesOfInstance(remainingInstances, items[i].GetInstanceIndex());
+    }
+  }
+
+
+  void SortedFrames::SortUsing3DLocation(std::set<size_t>& remainingInstances)
+  {
+    /**
+     * Compute the mean of the normal vectors, using the recursive
+     * formula for arithmetic means for numerical stability.
+     * https://diego.assencio.com/?index=c34d06f4f4de2375658ed41f70177d59
+     **/
+      
+    Vector meanNormal;
+    LinearAlgebra::AssignVector(meanNormal, 0, 0, 0);
+
+    unsigned int n = 0;
+
+    for (std::set<size_t>::const_iterator it = remainingInstances.begin();
+         it != remainingInstances.end(); ++it)
+    {
+      assert(instances_[*it] != NULL);
+      const Instance& instance = *instances_[*it];
+
+      if (instance.HasPosition())
+      {
+        n += 1;
+        meanNormal += (instance.GetNormal() - meanNormal) / static_cast<float>(n);
+      }
+    }
+
+    std::vector<SortableItem<float> > items;
+    items.reserve(n);
+      
+    for (std::set<size_t>::const_iterator it = remainingInstances.begin();
+         it != remainingInstances.end(); ++it)
+    {
+      assert(instances_[*it] != NULL);
+      const Instance& instance = *instances_[*it];
+        
+      std::string sopInstanceUid;
+      if (instance.HasPosition() &&
+          instance.GetTags().LookupStringValue(
+            sopInstanceUid, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false))
+      {
+        double p = LinearAlgebra::DotProduct(meanNormal, instance.GetPosition());
+        items.push_back(SortableItem<float>(p, *it, sopInstanceUid));
+      }
+    }
+
+    assert(items.size() <= n);
+    
+    std::sort(items.begin(), items.end());
+
+    for (size_t i = 0; i < items.size(); i++)
+    {
+      AddFramesOfInstance(remainingInstances, items[i].GetInstanceIndex());
+    }
+  }
+
+
+  size_t SortedFrames::GetFramesCount() const
+  {
+    if (sorted_)
+    {
+      return frames_.size();
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls,
+                                      "Sort() has not been called");
+    }
+  }
+
+
+  void SortedFrames::Sort()
+  {
+    if (!sorted_)
+    {
+      size_t totalFrames = 0;
+      std::set<size_t> remainingInstances;
+      
+      for (size_t i = 0; i < instances_.size(); i++)
+      {
+        assert(instances_[i] != NULL);
+        totalFrames += instances_[i]->GetNumberOfFrames();
+        
+        remainingInstances.insert(i);
+      }
+
+      frames_.clear();
+      frames_.reserve(totalFrames);
+
+      SortUsingIntegerTag(remainingInstances, Orthanc::DICOM_TAG_INSTANCE_NUMBER);  // VR is "IS"
+      SortUsingIntegerTag(remainingInstances, Orthanc::DICOM_TAG_IMAGE_INDEX);  // VR is "US"
+      SortUsing3DLocation(remainingInstances);
+      SortUsingSopInstanceUid(remainingInstances);
+
+      // The following could in theory happen if several instances
+      // have the same SOPInstanceUID, no ordering is available
+      for (std::set<size_t>::const_iterator it = remainingInstances.begin();
+           it != remainingInstances.end(); it++)
+      {
+        AddFramesOfInstance(remainingInstances, *it);
+      }
+
+      if (frames_.size() != totalFrames ||
+          !remainingInstances.empty())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+      
+      sorted_ = true;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Toolbox/SortedFrames.h	Thu Jun 18 16:01:00 2020 +0200
@@ -0,0 +1,174 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "LinearAlgebra.h"
+
+namespace OrthancStone
+{
+  class SortedFrames : public boost::noncopyable
+  {
+  private:
+    class Instance : public boost::noncopyable
+    {
+    private:
+      bool               hasPosition_;
+      Orthanc::DicomMap  tags_;
+      std::string        sopInstanceUid_;
+      unsigned int       numberOfFrames_;
+      Vector             normal_;
+      Vector             position_;
+
+    public:
+      Instance(const Orthanc::DicomMap& tags);
+
+      const Orthanc::DicomMap& GetTags() const
+      {
+        return tags_;
+      }
+
+      const std::string& GetSopInstanceUid() const
+      {
+        return sopInstanceUid_;
+      }
+
+      unsigned int GetNumberOfFrames() const
+      {
+        return numberOfFrames_;
+      }
+
+      bool HasPosition() const
+      {
+        return hasPosition_;
+      }
+
+      const Vector& GetNormal() const;
+
+      const Vector& GetPosition() const;
+    };
+
+    struct Frame
+    {
+    private:
+      const Instance&  instance_;
+      unsigned int     frameIndex_;
+
+    public:
+      Frame(const Instance& instance,
+            unsigned int frameIndex);
+
+      const Instance& GetInstance() const
+      {
+        return instance_;
+      }
+
+      unsigned int GetFrameIndex() const
+      {
+        return frameIndex_;
+      }
+    };
+
+    std::string             studyInstanceUid_;
+    std::string             seriesInstanceUid_;
+    std::vector<Instance*>  instances_;
+    std::vector<Frame>      frames_;
+    bool                    sorted_;
+
+    const Instance& GetInstance(size_t index) const;
+
+    const Frame& GetFrame(size_t index) const;
+
+    void AddFramesOfInstance(std::set<size_t>& remainingInstances,
+                             size_t index);
+
+    void SortUsingIntegerTag(std::set<size_t>& remainingInstances,
+                             const Orthanc::DicomTag& tag);
+
+    void SortUsingSopInstanceUid(std::set<size_t>& remainingInstances);
+
+    void SortUsing3DLocation(std::set<size_t>& remainingInstances);
+
+  public:
+    SortedFrames() :
+      sorted_(true)
+    {
+    }
+  
+    ~SortedFrames();
+
+    const std::string& GetStudyInstanceUid() const
+    {
+      return studyInstanceUid_;
+    }
+
+    const std::string& GetSeriesInstanceUid() const
+    {
+      return seriesInstanceUid_;
+    }
+
+    void AddInstance(const Orthanc::DicomMap& tags);
+
+    size_t GetInstancesCount() const
+    {
+      return instances_.size();
+    }
+
+    const Orthanc::DicomMap& GetInstanceTags(size_t index) const
+    {
+      return GetInstance(index).GetTags();
+    }
+
+    const std::string& GetSopInstanceUid(size_t index) const
+    {
+      return GetInstance(index).GetSopInstanceUid();
+    }
+
+    bool IsSorted() const
+    {
+      return sorted_;
+    }
+
+    size_t GetFramesCount() const;
+
+    const Orthanc::DicomMap& GetFrameTags(size_t index) const
+    {
+      return GetFrame(index).GetInstance().GetTags();
+    }
+
+    const std::string& GetFrameSopInstanceUid(size_t index) const
+    {
+      return GetFrame(index).GetInstance().GetSopInstanceUid();
+    }
+
+    const unsigned int GetFrameSiblingsCount(size_t index) const
+    {
+      return GetFrame(index).GetInstance().GetNumberOfFrames();
+    }
+
+    const unsigned int GetFrameIndex(size_t index) const
+    {
+      return GetFrame(index).GetFrameIndex();
+    }
+
+    void Sort();
+  };
+}
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Thu Jun 18 15:48:59 2020 +0200
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Thu Jun 18 16:01:00 2020 +0200
@@ -432,7 +432,7 @@
   ${ORTHANC_STONE_ROOT}/Framework/StoneEnumerations.cpp
   ${ORTHANC_STONE_ROOT}/Framework/StoneException.h
   ${ORTHANC_STONE_ROOT}/Framework/StoneInitialization.cpp
-  
+
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/AffineTransform2D.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/AffineTransform2D.h
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/CoordinateSystem3D.cpp
@@ -471,6 +471,8 @@
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/ShearWarpProjectiveTransform.h
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/SlicesSorter.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/SlicesSorter.h
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/SortedFrames.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Toolbox/SortedFrames.h
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/SubpixelReader.h
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/SubvoxelReader.h
   ${ORTHANC_STONE_ROOT}/Framework/Toolbox/TextRenderer.cpp
--- a/UnitTestsSources/SortedFramesTests.cpp	Thu Jun 18 15:48:59 2020 +0200
+++ b/UnitTestsSources/SortedFramesTests.cpp	Thu Jun 18 16:01:00 2020 +0200
@@ -21,479 +21,9 @@
 
 #include <gtest/gtest.h>
 
-#include <OrthancException.h>
-#include <DicomFormat/DicomMap.h>
-
-#include "../Framework/Toolbox/GeometryToolbox.h"
-
-namespace OrthancStone
-{
-  class SortedFrames : public boost::noncopyable
-  {
-  private:
-    class Instance : public boost::noncopyable
-    {
-    private:
-      bool               hasPosition_;
-      Orthanc::DicomMap  tags_;
-      std::string        sopInstanceUid_;
-      unsigned int       numberOfFrames_;
-      Vector             normal_;
-      Vector             position_;
-
-    public:
-      Instance(const Orthanc::DicomMap& tags)
-      {
-        tags_.Assign(tags);
-
-        if (!tags.LookupStringValue(sopInstanceUid_, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false))
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
-        }
-
-        uint32_t tmp;
-        if (tags.ParseUnsignedInteger32(tmp, Orthanc::DICOM_TAG_NUMBER_OF_FRAMES))
-        {
-          numberOfFrames_ = tmp;
-        }
-        else
-        {
-          numberOfFrames_ = 1;
-        }
-
-        hasPosition_ = (
-          LinearAlgebra::ParseVector(position_, tags, Orthanc::DICOM_TAG_IMAGE_POSITION_PATIENT) &&
-          position_.size() == 3 &&
-          GeometryToolbox::ComputeNormal(normal_, tags));
-      }
-
-      const Orthanc::DicomMap& GetTags() const
-      {
-        return tags_;
-      }
-
-      const std::string& GetSopInstanceUid() const
-      {
-        return sopInstanceUid_;
-      }
-
-      unsigned int GetNumberOfFrames() const
-      {
-        return numberOfFrames_;
-      }
-
-      bool HasPosition() const
-      {
-        return hasPosition_;
-      }
-
-      const Vector& GetNormal() const
-      {
-        if (hasPosition_)
-        {
-          return normal_;
-        }
-        else
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-        }
-      }
-
-      const Vector& GetPosition() const
-      {
-        if (hasPosition_)
-        {
-          return position_;
-        }
-        else
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
-        }
-      }
-    };
-
-    struct Frame
-    {
-    private:
-      const Instance&  instance_;
-      unsigned int     frameIndex_;
-
-    public:
-      Frame(const Instance& instance,
-            unsigned int frameIndex) :
-        instance_(instance),
-        frameIndex_(frameIndex)
-      {
-        if (frameIndex >= instance.GetNumberOfFrames())
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-        }
-      }
-
-      const Instance& GetInstance() const
-      {
-        return instance_;
-      }
-
-      unsigned int GetFrameIndex() const
-      {
-        return frameIndex_;
-      }
-    };
-
-    std::string             studyInstanceUid_;
-    std::string             seriesInstanceUid_;
-    std::vector<Instance*>  instances_;
-    std::vector<Frame>      frames_;
-    bool                    sorted_;
-
-    const Instance& GetInstance(size_t index) const
-    {
-      if (index >= instances_.size())
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-      }
-      else
-      {
-        assert(instances_[index] != NULL);
-        return *instances_[index];
-      }
-    }
-
-    const Frame& GetFrame(size_t index) const
-    {
-      if (!sorted_)
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls,
-                                        "Sort() has not been called");
-      }
-      if (index >= frames_.size())
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-      }
-      else
-      {
-        return frames_[index];
-      }    
-    }
-
-  public:
-    SortedFrames() :
-      sorted_(true)
-    {
-    }
-  
-    ~SortedFrames()
-    {
-      for (size_t i = 0; i < instances_.size(); i++)
-      {
-        assert(instances_[i] != NULL);
-        delete instances_[i];
-      }
-    }
-
-    const std::string& GetStudyInstanceUid() const
-    {
-      return studyInstanceUid_;
-    }
-
-    const std::string& GetSeriesInstanceUid() const
-    {
-      return seriesInstanceUid_;
-    }
-
-    void AddInstance(const Orthanc::DicomMap& tags)
-    {
-      std::unique_ptr<Instance> instance(new Instance(tags));
-
-      std::string studyInstanceUid, seriesInstanceUid;
-      if (!tags.LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) ||
-          !tags.LookupStringValue(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false))
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
-      }
-    
-      if (instances_.empty())
-      {
-        studyInstanceUid_ = studyInstanceUid;
-        seriesInstanceUid_ = seriesInstanceUid;
-      }
-      else
-      {
-        if (studyInstanceUid_ != studyInstanceUid ||
-            seriesInstanceUid_ != seriesInstanceUid)
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
-                                          "Mixing instances from different series");
-        }
-      }
-
-      instances_.push_back(instance.release());
-      sorted_ = false;
-      frames_.clear();
-    }
-
-    size_t GetInstancesCount() const
-    {
-      return instances_.size();
-    }
-
-    const Orthanc::DicomMap& GetInstanceTags(size_t index) const
-    {
-      return GetInstance(index).GetTags();
-    }
-
-    const std::string& GetSopInstanceUid(size_t index) const
-    {
-      return GetInstance(index).GetSopInstanceUid();
-    }
-
-    bool IsSorted() const
-    {
-      return sorted_;
-    }
+#include "../Framework/Toolbox/SortedFrames.h"
 
-    size_t GetFramesCount() const
-    {
-      if (sorted_)
-      {
-        return frames_.size();
-      }
-      else
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls,
-                                        "Sort() has not been called");
-      }
-    }
-
-    const Orthanc::DicomMap& GetFrameTags(size_t index) const
-    {
-      return GetFrame(index).GetInstance().GetTags();
-    }
-
-    const std::string& GetFrameSopInstanceUid(size_t index) const
-    {
-      return GetFrame(index).GetInstance().GetSopInstanceUid();
-    }
-
-    const unsigned int GetFrameSiblingsCount(size_t index) const
-    {
-      return GetFrame(index).GetInstance().GetNumberOfFrames();
-    }
-
-    const unsigned int GetFrameIndex(size_t index) const
-    {
-      return GetFrame(index).GetFrameIndex();
-    }
-
-
-    void Sort()
-    {
-      if (!sorted_)
-      {
-        size_t totalFrames = 0;
-        std::set<size_t> remainingInstances;
-      
-        for (size_t i = 0; i < instances_.size(); i++)
-        {
-          assert(instances_[i] != NULL);
-          totalFrames += instances_[i]->GetNumberOfFrames();
-        
-          remainingInstances.insert(i);
-        }
-
-        frames_.clear();
-        frames_.reserve(totalFrames);
-
-        SortUsingIntegerTag(remainingInstances, Orthanc::DICOM_TAG_INSTANCE_NUMBER);  // VR is "IS"
-        SortUsingIntegerTag(remainingInstances, Orthanc::DICOM_TAG_IMAGE_INDEX);  // VR is "US"
-        SortUsing3DLocation(remainingInstances);
-        SortUsingSopInstanceUid(remainingInstances);
-
-        // The following could in theory happen if several instances
-        // have the same SOPInstanceUID, no ordering is available
-        for (std::set<size_t>::const_iterator it = remainingInstances.begin();
-             it != remainingInstances.end(); it++)
-        {
-          AddFramesOfInstance(remainingInstances, *it);
-        }
-
-        if (frames_.size() != totalFrames ||
-            !remainingInstances.empty())
-        {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
-        }
-      
-        sorted_ = true;
-      }
-    }
-
-
-  private:
-    void AddFramesOfInstance(std::set<size_t>& remainingInstances,
-                             size_t index)
-    {
-      assert(instances_[index] != NULL);
-      const Instance& instance = *instances_[index];
-    
-      for (unsigned int i = 0; i < instance.GetNumberOfFrames(); i++)
-      {
-        frames_.push_back(Frame(instance, i));
-      }
-
-      assert(remainingInstances.find(index) != remainingInstances.end());
-      remainingInstances.erase(index);
-    }
-
-
-    template<typename T>
-    class SortableItem
-    {
-    private:
-      T            value_;
-      size_t       instance_;
-      std::string  sopInstanceUid_;
-
-    public:
-      SortableItem(const T& value,
-                   size_t instance,
-                   const std::string& sopInstanceUid) :
-        value_(value),
-        instance_(instance),
-        sopInstanceUid_(sopInstanceUid)
-      {
-      }
-
-      size_t GetInstanceIndex() const
-      {
-        return instance_;
-      }
-
-      bool operator< (const SortableItem& other) const
-      {
-        return (value_ < other.value_ ||
-                (value_ == other.value_ &&
-                 sopInstanceUid_ < other.sopInstanceUid_));
-      }
-    };
-
-
-    void SortUsingIntegerTag(std::set<size_t>& remainingInstances,
-                             const Orthanc::DicomTag& tag)
-    {
-      std::vector< SortableItem<int32_t> > items;
-      items.reserve(remainingInstances.size());
-
-      for (std::set<size_t>::const_iterator it = remainingInstances.begin();
-           it != remainingInstances.end(); ++it)
-      {
-        assert(instances_[*it] != NULL);
-        const Instance& instance = *instances_[*it];
-
-        int32_t value;
-        std::string sopInstanceUid;
-        if (instance.GetTags().ParseInteger32(value, tag) &&
-            instance.GetTags().LookupStringValue(
-              sopInstanceUid, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false))
-        {
-          items.push_back(SortableItem<int32_t>(value, *it, sopInstanceUid));
-        }
-      }
-    
-      std::sort(items.begin(), items.end());
-
-      for (size_t i = 0; i < items.size(); i++)
-      {
-        AddFramesOfInstance(remainingInstances, items[i].GetInstanceIndex());
-      }
-    }
-
-
-    void SortUsingSopInstanceUid(std::set<size_t>& remainingInstances)
-    {
-      std::vector<SortableItem<int32_t> > items;
-      items.reserve(remainingInstances.size());
-
-      for (std::set<size_t>::const_iterator it = remainingInstances.begin();
-           it != remainingInstances.end(); ++it)
-      {
-        assert(instances_[*it] != NULL);
-        const Instance& instance = *instances_[*it];
-
-        std::string sopInstanceUid;
-        if (instance.GetTags().LookupStringValue(
-              sopInstanceUid, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false))
-        {
-          items.push_back(SortableItem<int32_t>(0 /* arbitrary value */, *it, sopInstanceUid));
-        }
-      }
-    
-      std::sort(items.begin(), items.end());
-
-      for (size_t i = 0; i < items.size(); i++)
-      {
-        AddFramesOfInstance(remainingInstances, items[i].GetInstanceIndex());
-      }
-    }
-
-
-    void SortUsing3DLocation(std::set<size_t>& remainingInstances)
-    {
-      /**
-       * Compute the mean of the normal vectors, using the recursive
-       * formula for arithmetic means for numerical stability.
-       * https://diego.assencio.com/?index=c34d06f4f4de2375658ed41f70177d59
-       **/
-      
-      Vector meanNormal;
-      LinearAlgebra::AssignVector(meanNormal, 0, 0, 0);
-
-      unsigned int n = 0;
-
-      for (std::set<size_t>::const_iterator it = remainingInstances.begin();
-           it != remainingInstances.end(); ++it)
-      {
-        assert(instances_[*it] != NULL);
-        const Instance& instance = *instances_[*it];
-
-        if (instance.HasPosition())
-        {
-          n += 1;
-          meanNormal += (instance.GetNormal() - meanNormal) / static_cast<float>(n);
-        }
-      }
-
-      std::vector<SortableItem<float> > items;
-      items.reserve(n);
-      
-      for (std::set<size_t>::const_iterator it = remainingInstances.begin();
-           it != remainingInstances.end(); ++it)
-      {
-        assert(instances_[*it] != NULL);
-        const Instance& instance = *instances_[*it];
-        
-        std::string sopInstanceUid;
-        if (instance.HasPosition() &&
-            instance.GetTags().LookupStringValue(
-              sopInstanceUid, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false))
-        {
-          double p = LinearAlgebra::DotProduct(meanNormal, instance.GetPosition());
-          items.push_back(SortableItem<float>(p, *it, sopInstanceUid));
-        }
-      }
-
-      assert(items.size() <= n);
-    
-      std::sort(items.begin(), items.end());
-      printf(">> %d\n", items.size());
-
-      for (size_t i = 0; i < items.size(); i++)
-      {
-        AddFramesOfInstance(remainingInstances, items[i].GetInstanceIndex());
-      }
-    }
-  };
-}
+#include <OrthancException.h>
 
 
 TEST(SortedFrames, Basic)