diff Framework/Toolbox/OrthancSeriesLoader.cpp @ 0:351ab0da0150

initial commit
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 14 Oct 2016 15:34:11 +0200
parents
children ff1e935768e7
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Toolbox/OrthancSeriesLoader.cpp	Fri Oct 14 15:34:11 2016 +0200
@@ -0,0 +1,444 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OrthancSeriesLoader.h"
+
+#include "../Messaging/MessagingToolbox.h"
+#include "../Orthanc/Core/Images/Image.h"
+#include "../Orthanc/Core/Images/ImageProcessing.h"
+#include "../Orthanc/Core/Logging.h"
+#include "../Orthanc/Core/OrthancException.h"
+#include "DicomFrameConverter.h"
+
+namespace OrthancStone
+{
+  class OrthancSeriesLoader::Slice : public boost::noncopyable
+  {
+  private:
+    std::string     instanceId_;
+    SliceGeometry   geometry_;
+    double          projectionAlongNormal_;
+
+  public:
+    Slice(const std::string& instanceId,
+          const std::string& imagePositionPatient,
+          const std::string& imageOrientationPatient) :
+      instanceId_(instanceId),
+      geometry_(imagePositionPatient, imageOrientationPatient)
+    {
+    }
+
+    const std::string GetInstanceId() const
+    {
+      return instanceId_;
+    }
+
+    const SliceGeometry& GetGeometry() const
+    {
+      return geometry_;
+    }
+
+    void SetNormal(const Vector& normal)
+    {
+      projectionAlongNormal_ = boost::numeric::ublas::inner_prod(geometry_.GetOrigin(), normal);
+    }
+
+    double GetProjectionAlongNormal() const
+    {
+      return projectionAlongNormal_;
+    }
+  };
+
+
+  class OrthancSeriesLoader::SetOfSlices : public boost::noncopyable
+  {
+  private:
+    std::vector<Slice*>  slices_;
+
+    struct Comparator
+    {
+      bool operator() (const Slice* const a,
+                       const Slice* const b) const
+      {
+        return a->GetProjectionAlongNormal() < b->GetProjectionAlongNormal();
+      }
+    };
+
+  public:
+    ~SetOfSlices()
+    {
+      for (size_t i = 0; i < slices_.size(); i++)
+      {
+        assert(slices_[i] != NULL);
+        delete slices_[i];
+      }
+    }
+
+    void Reserve(size_t size)
+    {
+      slices_.reserve(size);
+    }
+
+    void AddSlice(const std::string& instanceId,
+                  const std::string& imagePositionPatient,
+                  const std::string& imageOrientationPatient)
+    {
+      slices_.push_back(new Slice(instanceId, imagePositionPatient, imageOrientationPatient));
+    }
+
+    size_t GetSliceCount() const
+    {
+      return slices_.size();
+    }
+
+    const Slice& GetSlice(size_t index) const
+    {
+      assert(slices_[index] != NULL);
+      return *slices_[index];
+    }
+      
+    void Sort(const Vector& normal)
+    {
+      for (size_t i = 0; i < slices_.size(); i++)
+      {
+        slices_[i]->SetNormal(normal);
+      }
+
+      Comparator comparator;
+      std::sort(slices_.begin(), slices_.end(), comparator);
+    }
+
+    void LoadSeriesFast(IOrthancConnection&  orthanc,
+                        const std::string& series)
+    {
+      // Retrieve the orientation of this series
+      Json::Value info;
+      MessagingToolbox::RestApiGet(info, orthanc, "/series/" + series);
+
+      if (info.type() != Json::objectValue ||
+          !info.isMember("MainDicomTags") ||
+          info["MainDicomTags"].type() != Json::objectValue ||
+          !info["MainDicomTags"].isMember("ImageOrientationPatient") ||
+          info["MainDicomTags"]["ImageOrientationPatient"].type() != Json::stringValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+
+      std::string imageOrientationPatient = info["MainDicomTags"]["ImageOrientationPatient"].asString();
+
+
+      // Retrieve the Orthanc ID of all the instances of this series
+      Json::Value instances;
+      MessagingToolbox::RestApiGet(instances, orthanc, "/series/" + series + "/instances");
+
+      if (instances.type() != Json::arrayValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+
+      if (instances.size() == 0)
+      {
+        LOG(ERROR) << "This series is empty";
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+      }
+
+
+      // Retrieve the DICOM tags of all the instances
+      std::vector<std::string> instancesId;
+
+      instancesId.resize(instances.size());
+      Reserve(instances.size());
+
+      for (Json::Value::ArrayIndex i = 0; i < instances.size(); i++)
+      {
+        if (instances[i].type() != Json::objectValue ||
+            !instances[i].isMember("ID") ||
+            !instances[i].isMember("MainDicomTags") ||
+            instances[i]["ID"].type() != Json::stringValue ||
+            instances[i]["MainDicomTags"].type() != Json::objectValue ||
+            !instances[i]["MainDicomTags"].isMember("ImagePositionPatient") ||
+            instances[i]["MainDicomTags"]["ImagePositionPatient"].type() != Json::stringValue)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+        else
+        {
+          instancesId[i] = instances[i]["ID"].asString();
+          AddSlice(instancesId[i], 
+                   instances[i]["MainDicomTags"]["ImagePositionPatient"].asString(),
+                   imageOrientationPatient);
+        }
+      }
+
+      assert(GetSliceCount() == instances.size());
+    }
+
+      
+    void LoadSeriesSafe(IOrthancConnection& orthanc,
+                        const std::string& seriesId)
+    {
+      Json::Value series;
+      MessagingToolbox::RestApiGet(series, orthanc, "/series/" + seriesId + "/instances-tags?simplify");
+
+      if (series.type() != Json::objectValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+
+      if (series.size() == 0)
+      {
+        LOG(ERROR) << "This series is empty";
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+      }
+
+      Json::Value::Members instances = series.getMemberNames();
+
+      Reserve(instances.size());
+
+      for (Json::Value::ArrayIndex i = 0; i < instances.size(); i++)
+      {
+        const Json::Value& tags = series[instances[i]];
+
+        if (tags.type() != Json::objectValue ||
+            !tags.isMember("ImagePositionPatient") ||
+            !tags.isMember("ImageOrientationPatient") ||
+            tags["ImagePositionPatient"].type() != Json::stringValue ||
+            tags["ImageOrientationPatient"].type() != Json::stringValue)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        }
+        else
+        {
+          AddSlice(instances[i], 
+                   tags["ImagePositionPatient"].asString(),
+                   tags["ImageOrientationPatient"].asString());
+        }
+      }
+
+      assert(GetSliceCount() == instances.size());
+    }
+
+      
+    void SelectNormal(Vector& normal) const
+    {
+      std::vector<Vector>  normalCandidates;
+      std::vector<unsigned int>  normalCount;
+
+      bool found = false;
+
+      for (size_t i = 0; !found && i < GetSliceCount(); i++)
+      {
+        const Vector& normal = GetSlice(i).GetGeometry().GetNormal();
+
+        bool add = true;
+        for (size_t j = 0; add && j < normalCandidates.size(); j++)  // (*)
+        {
+          if (GeometryToolbox::IsParallel(normal, normalCandidates[j]))
+          {
+            normalCount[j] += 1;
+            add = false;
+          }
+        }
+
+        if (add)
+        {
+          if (normalCount.size() > 2)
+          {
+            // To get linear-time complexity in (*). This heuristics
+            // allows the series to have one single frame that is
+            // not parallel to the others (such a frame could be a
+            // generated preview)
+            found = false;
+          }
+          else
+          {
+            normalCandidates.push_back(normal);
+            normalCount.push_back(1);
+          }
+        }
+      }
+
+      for (size_t i = 0; !found && i < normalCandidates.size(); i++)
+      {
+        unsigned int count = normalCount[i];
+        if (count == GetSliceCount() ||
+            count + 1 == GetSliceCount())
+        {
+          normal = normalCandidates[i];
+          found = true;
+        }
+      }
+
+      if (!found)
+      {
+        LOG(ERROR) << "Cannot select a normal that is shared by most of the slices of this series";
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);          
+      }
+    }
+
+
+    void FilterNormal(const Vector& normal)
+    {
+      size_t pos = 0;
+
+      for (size_t i = 0; i < slices_.size(); i++)
+      {
+        if (GeometryToolbox::IsParallel(normal, slices_[i]->GetGeometry().GetNormal()))
+        {
+          // This slice is compatible with the selected normal
+          slices_[pos] = slices_[i];
+          pos += 1;
+        }
+        else
+        {
+          delete slices_[i];
+          slices_[i] = NULL;
+        }
+      }
+
+      slices_.resize(pos);
+    }
+  };
+
+
+
+  OrthancSeriesLoader::OrthancSeriesLoader(IOrthancConnection& orthanc,
+                                           const std::string& series) :
+    orthanc_(orthanc),
+    slices_(new SetOfSlices)
+  {
+    /**
+     * The function "LoadSeriesFast()" might now behave properly if
+     * some slice has some outsider value for its normal, which
+     * happens sometimes on reprojected series (e.g. coronal and
+     * sagittal of Delphine). Don't use it.
+     **/
+
+    slices_->LoadSeriesSafe(orthanc, series);
+    
+    Vector normal;
+    slices_->SelectNormal(normal);
+    slices_->FilterNormal(normal);
+    slices_->Sort(normal);
+
+    if (slices_->GetSliceCount() == 0)  // Sanity check
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    for (size_t i = 0; i < slices_->GetSliceCount(); i++)
+    {
+      assert(GeometryToolbox::IsParallel(normal, slices_->GetSlice(i).GetGeometry().GetNormal()));
+      geometry_.AddSlice(slices_->GetSlice(i).GetGeometry());
+    }
+
+    std::auto_ptr<DicomDataset> dataset(new DicomDataset(orthanc_, slices_->GetSlice(0).GetInstanceId()));
+    if (!dataset->HasTag(DICOM_TAG_ROWS) ||
+        !dataset->HasTag(DICOM_TAG_COLUMNS))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentTag);
+    }
+
+    DicomFrameConverter converter;
+    converter.ReadParameters(*dataset);
+
+    format_ = converter.GetExpectedPixelFormat();
+    width_ = dataset->GetUnsignedIntegerValue(DICOM_TAG_COLUMNS);
+    height_ = dataset->GetUnsignedIntegerValue(DICOM_TAG_ROWS);
+  }
+    
+
+  DicomDataset* OrthancSeriesLoader::DownloadDicom(size_t index)
+  {
+    std::auto_ptr<DicomDataset> dataset(new DicomDataset(orthanc_, slices_->GetSlice(index).GetInstanceId()));
+
+    if (dataset->HasTag(DICOM_TAG_NUMBER_OF_FRAMES) &&
+        dataset->GetUnsignedIntegerValue(DICOM_TAG_NUMBER_OF_FRAMES) != 1)
+    {
+      LOG(ERROR) << "One instance in this series has more than 1 frame";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);          
+    }
+
+    return dataset.release();
+  }
+
+
+  void OrthancSeriesLoader::CheckFrame(const Orthanc::ImageAccessor& frame) const
+  {
+    if (frame.GetFormat() != format_ ||
+        frame.GetWidth() != width_ ||
+        frame.GetHeight() != height_)
+    {
+      LOG(ERROR) << "The parameters of this series vary accross its slices";
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);          
+    }
+  }
+
+
+  Orthanc::ImageAccessor* OrthancSeriesLoader::DownloadFrame(size_t index)
+  {
+    const Slice& slice = slices_->GetSlice(index);
+
+    std::auto_ptr<Orthanc::ImageAccessor> frame
+      (MessagingToolbox::DecodeFrame(orthanc_, slice.GetInstanceId(), 0, format_));
+
+    if (frame.get() != NULL)
+    {
+      CheckFrame(*frame);
+    }
+
+    return frame.release();
+  }
+
+
+  Orthanc::ImageAccessor* OrthancSeriesLoader::DownloadJpegFrame(size_t index,
+                                                                 unsigned int quality)
+  {
+    const Slice& slice = slices_->GetSlice(index);
+
+    std::auto_ptr<Orthanc::ImageAccessor> frame
+      (MessagingToolbox::DecodeJpegFrame(orthanc_, slice.GetInstanceId(), 0, quality, format_));
+
+    if (frame.get() != NULL)
+    {
+      CheckFrame(*frame);
+    }
+
+    return frame.release();
+  }
+
+
+  bool OrthancSeriesLoader::IsJpegAvailable()
+  {
+    return MessagingToolbox::HasWebViewerInstalled(orthanc_);
+  }
+}