view Framework/Toolbox/OrthancSeriesLoader.cpp @ 2160:66ce58986217 legacy

closing legacy
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 27 Sep 2024 22:33:15 +0200
parents 4cff7b1ed31d
children
line wrap: on
line source

/**
 * Stone of Orthanc
 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 * Department, University Hospital of Liege, Belgium
 * Copyright (C) 2017-2018 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 "OrthancSeriesLoader.h"

#include "../Toolbox/MessagingToolbox.h"
#include "../../Resources/Orthanc/Core/Images/Image.h"
#include "../../Resources/Orthanc/Core/Images/ImageProcessing.h"
#include "../../Resources/Orthanc/Core/Logging.h"
#include "../../Resources/Orthanc/Core/OrthancException.h"
#include "../../Resources/Orthanc/Plugins/Samples/Common/FullOrthancDataset.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(OrthancPlugins::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(OrthancPlugins::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(OrthancPlugins::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::string uri = "/instances/" + slices_->GetSlice(0).GetInstanceId() + "/tags";

    OrthancPlugins::FullOrthancDataset dataset(orthanc_, uri);
    OrthancPlugins::DicomDatasetReader reader(dataset);

    if (!reader.GetUnsignedIntegerValue(width_, OrthancPlugins::DICOM_TAG_COLUMNS) ||
        !reader.GetUnsignedIntegerValue(height_, OrthancPlugins::DICOM_TAG_ROWS))
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentTag);
    }

    DicomFrameConverter converter;
    converter.ReadParameters(dataset);
    format_ = converter.GetExpectedPixelFormat();
  }
    

  OrthancPlugins::IDicomDataset* OrthancSeriesLoader::DownloadDicom(size_t index)
  {
    std::string uri = "/instances/" + slices_->GetSlice(index).GetInstanceId() + "/tags";

    std::auto_ptr<OrthancPlugins::IDicomDataset> dataset(new OrthancPlugins::FullOrthancDataset(orthanc_, uri));
    OrthancPlugins::DicomDatasetReader reader(*dataset);

    unsigned int frames;
    if (reader.GetUnsignedIntegerValue(frames, OrthancPlugins::DICOM_TAG_NUMBER_OF_FRAMES) &&
        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_);
  }
}