Mercurial > hg > orthanc-stone
view UnitTestsSources/UnitTestsMain.cpp @ 69:1553b67b24e5 wasm
OrthancSynchronousWebService
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Mon, 22 May 2017 20:35:11 +0200 |
parents | 1526d38ef6da |
children | f73aed014bde |
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 Osimis, 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 "gtest/gtest.h" #include "../Resources/Orthanc/Core/Logging.h" #include "../Framework/Toolbox/OrthancSynchronousWebService.h" #include "../Framework/Layers/OrthancFrameLayerSource.h" #include "../Framework/Widgets/LayerWidget.h" #include "../Resources/Orthanc/Core/Images/PngReader.h" #include "../Framework/Toolbox/MessagingToolbox.h" #include "../Framework/Toolbox/DicomFrameConverter.h" #include <boost/lexical_cast.hpp> namespace OrthancStone { class Slice { private: enum Type { Type_Invalid, Type_OrthancInstance // TODO A slice could come from some DICOM file (URL) }; Type type_; std::string orthancInstanceId_; unsigned int frame_; SliceGeometry geometry_; double pixelSpacingX_; double pixelSpacingY_; double thickness_; unsigned int width_; unsigned int height_; DicomFrameConverter converter_; public: Slice() : type_(Type_Invalid) { } bool ParseOrthancFrame(const OrthancPlugins::IDicomDataset& dataset, const std::string& instanceId, unsigned int frame) { OrthancPlugins::DicomDatasetReader reader(dataset); unsigned int frameCount; if (!reader.GetUnsignedIntegerValue(frameCount, OrthancPlugins::DICOM_TAG_NUMBER_OF_FRAMES)) { frameCount = 1; // Assume instance with one frame } if (frame >= frameCount) { return false; } if (!reader.GetDoubleValue(thickness_, OrthancPlugins::DICOM_TAG_SLICE_THICKNESS)) { thickness_ = 100.0 * std::numeric_limits<double>::epsilon(); } GeometryToolbox::GetPixelSpacing(pixelSpacingX_, pixelSpacingY_, dataset); std::string position, orientation; if (dataset.GetStringValue(position, OrthancPlugins::DICOM_TAG_IMAGE_POSITION_PATIENT) && dataset.GetStringValue(orientation, OrthancPlugins::DICOM_TAG_IMAGE_ORIENTATION_PATIENT) && reader.GetUnsignedIntegerValue(width_, OrthancPlugins::DICOM_TAG_COLUMNS) && reader.GetUnsignedIntegerValue(height_, OrthancPlugins::DICOM_TAG_ROWS)) { orthancInstanceId_ = instanceId; frame_ = frame; geometry_ = SliceGeometry(position, orientation); converter_.ReadParameters(dataset); type_ = Type_OrthancInstance; return true; } else { return false; } } bool IsOrthancInstance() const { return type_ == Type_OrthancInstance; } const std::string GetOrthancInstanceId() const { if (type_ != Type_OrthancInstance) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } return orthancInstanceId_; } unsigned int GetFrame() const { if (type_ == Type_Invalid) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } return frame_; } const SliceGeometry& GetGeometry() const { if (type_ == Type_Invalid) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } return geometry_; } double GetThickness() const { if (type_ == Type_Invalid) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } return thickness_; } double GetPixelSpacingX() const { if (type_ == Type_Invalid) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } return pixelSpacingX_; } double GetPixelSpacingY() const { if (type_ == Type_Invalid) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } return pixelSpacingY_; } unsigned int GetWidth() const { if (type_ == Type_Invalid) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } return width_; } unsigned int GetHeight() const { if (type_ == Type_Invalid) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } return height_; } const DicomFrameConverter& GetConverter() const { if (type_ == Type_Invalid) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } return converter_; } }; class SliceSorter : public boost::noncopyable { private: class SliceWithDepth : public boost::noncopyable { private: Slice slice_; double depth_; public: SliceWithDepth(const Slice& slice) : slice_(slice), depth_(0) { } void SetNormal(const Vector& normal) { depth_ = boost::numeric::ublas::inner_prod (slice_.GetGeometry().GetOrigin(), normal); } double GetDepth() const { return depth_; } const Slice& GetSlice() const { return slice_; } }; struct Comparator { bool operator() (const SliceWithDepth* const& a, const SliceWithDepth* const& b) const { return a->GetDepth() < b->GetDepth(); } }; typedef std::vector<SliceWithDepth*> Slices; Slices slices_; bool hasNormal_; public: SliceSorter() : hasNormal_(false) { } ~SliceSorter() { for (size_t i = 0; i < slices_.size(); i++) { assert(slices_[i] != NULL); delete slices_[i]; } } void Reserve(size_t count) { slices_.reserve(count); } void AddSlice(const Slice& slice) { slices_.push_back(new SliceWithDepth(slice)); } size_t GetSliceCount() const { return slices_.size(); } const Slice& GetSlice(size_t i) const { if (i >= slices_.size()) { throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } assert(slices_[i] != NULL); return slices_[i]->GetSlice(); } void SetNormal(const Vector& normal) { for (size_t i = 0; i < slices_.size(); i++) { slices_[i]->SetNormal(normal); } hasNormal_ = true; } void Sort() { if (!hasNormal_) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } Comparator comparator; std::sort(slices_.begin(), slices_.end(), comparator); } void FilterNormal(const Vector& normal) { size_t pos = 0; for (size_t i = 0; i < slices_.size(); i++) { if (GeometryToolbox::IsParallel(normal, slices_[i]->GetSlice().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); } bool 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; } } return found; } }; class OrthancSliceLoader : public IWebService::ICallback // TODO move to PImpl { public: class ICallback : public boost::noncopyable { public: virtual ~ICallback() { } virtual void NotifyGeometryReady(const OrthancSliceLoader& loader) = 0; virtual void NotifyGeometryError(const OrthancSliceLoader& loader) = 0; virtual void NotifySliceImageReady(const OrthancSliceLoader& loader, unsigned int sliceIndex, const Slice& slice, Orthanc::ImageAccessor* image) = 0; virtual void NotifySliceImageError(const OrthancSliceLoader& loader, unsigned int sliceIndex, const Slice& slice) = 0; }; private: enum State { State_Error, State_Initialization, State_LoadingGeometry, State_GeometryReady }; enum Mode { Mode_SeriesGeometry, Mode_LoadImage }; class Operation : public Orthanc::IDynamicObject { private: Mode mode_; unsigned int sliceIndex_; const Slice* slice_; Operation(Mode mode) : mode_(mode) { } public: Mode GetMode() const { return mode_; } unsigned int GetSliceIndex() const { assert(mode_ == Mode_LoadImage); return sliceIndex_; } const Slice& GetSlice() const { assert(mode_ == Mode_LoadImage && slice_ != NULL); return *slice_; } static Operation* DownloadSeriesGeometry() { return new Operation(Mode_SeriesGeometry); } static Operation* DownloadSliceImage(unsigned int sliceIndex, const Slice& slice) { std::auto_ptr<Operation> tmp(new Operation(Mode_LoadImage)); tmp->sliceIndex_ = sliceIndex; tmp->slice_ = &slice; return tmp.release(); } }; ICallback& callback_; IWebService& orthanc_; State state_; SliceSorter slices_; void ParseSeriesGeometry(const void* answer, size_t size) { Json::Value series; if (!MessagingToolbox::ParseJson(series, answer, size) || series.type() != Json::objectValue) { callback_.NotifyGeometryError(*this); return; } Json::Value::Members instances = series.getMemberNames(); slices_.Reserve(instances.size()); for (size_t i = 0; i < instances.size(); i++) { OrthancPlugins::FullOrthancDataset dataset(series[instances[i]]); Slice slice; if (slice.ParseOrthancFrame(dataset, instances[i], 0 /* todo */)) { slices_.AddSlice(slice); } else { LOG(WARNING) << "Skipping invalid instance " << instances[i]; } } bool ok = false; if (slices_.GetSliceCount() > 0) { Vector normal; if (slices_.SelectNormal(normal)) { slices_.FilterNormal(normal); slices_.SetNormal(normal); slices_.Sort(); ok = true; } } if (ok) { LOG(INFO) << "Loaded a series with " << slices_.GetSliceCount() << " slice(s)"; callback_.NotifyGeometryReady(*this); } else { LOG(ERROR) << "This series is empty"; callback_.NotifyGeometryError(*this); } } void ParseSliceImage(const Operation& operation, const void* answer, size_t size) { std::auto_ptr<Orthanc::PngReader> image(new Orthanc::PngReader); image->ReadFromMemory(answer, size); bool ok = (image->GetWidth() == operation.GetSlice().GetWidth() || image->GetHeight() == operation.GetSlice().GetHeight()); if (ok && operation.GetSlice().GetConverter().GetExpectedPixelFormat() == Orthanc::PixelFormat_SignedGrayscale16) { if (image->GetFormat() == Orthanc::PixelFormat_Grayscale16) { image->SetFormat(Orthanc::PixelFormat_SignedGrayscale16); } else { ok = false; } } if (ok) { callback_.NotifySliceImageReady(*this, operation.GetSliceIndex(), operation.GetSlice(), image.release()); } else { callback_.NotifySliceImageError(*this, operation.GetSliceIndex(), operation.GetSlice()); } } public: OrthancSliceLoader(ICallback& callback, IWebService& orthanc) : callback_(callback), orthanc_(orthanc), state_(State_Initialization) { } void ScheduleLoadSeries(const std::string& seriesId) { if (state_ != State_Initialization) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } else { state_ = State_LoadingGeometry; std::string uri = "/series/" + seriesId + "/instances-tags"; orthanc_.ScheduleGetRequest(*this, uri, Operation::DownloadSeriesGeometry()); } } size_t GetSliceCount() const { if (state_ != State_GeometryReady) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } return slices_.GetSliceCount(); } const Slice& GetSlice(size_t index) const { if (state_ != State_GeometryReady) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } return slices_.GetSlice(index); } void ScheduleLoadSliceImage(size_t index) { if (state_ != State_GeometryReady) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } else { const Slice& slice = GetSlice(index); std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" + boost::lexical_cast<std::string>(slice.GetFrame())); switch (slice.GetConverter().GetExpectedPixelFormat()) { case Orthanc::PixelFormat_RGB24: uri += "/preview"; break; case Orthanc::PixelFormat_Grayscale16: uri += "/image-uint16"; break; case Orthanc::PixelFormat_SignedGrayscale16: uri += "/image-int16"; break; default: throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } orthanc_.ScheduleGetRequest(*this, uri, Operation::DownloadSliceImage(index, slice)); } } virtual void NotifySuccess(const std::string& uri, const void* answer, size_t answerSize, Orthanc::IDynamicObject* payload) { std::auto_ptr<Operation> operation(dynamic_cast<Operation*>(payload)); switch (operation->GetMode()) { case Mode_SeriesGeometry: ParseSeriesGeometry(answer, answerSize); state_ = State_GeometryReady; break; case Mode_LoadImage: ParseSliceImage(*operation, answer, answerSize); break; default: throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); } } virtual void NotifyError(const std::string& uri, Orthanc::IDynamicObject* payload) { std::auto_ptr<Operation> operation(dynamic_cast<Operation*>(payload)); LOG(ERROR) << "Cannot download " << uri; switch (operation->GetMode()) { case Mode_SeriesGeometry: callback_.NotifyGeometryError(*this); state_ = State_Error; break; case Mode_LoadImage: callback_.NotifySliceImageError(*this, operation->GetSliceIndex(), operation->GetSlice()); break; default: throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); } } }; class Tata : public OrthancSliceLoader::ICallback { public: virtual void NotifyGeometryReady(const OrthancSliceLoader& loader) { printf("Done\n"); } virtual void NotifyGeometryError(const OrthancSliceLoader& loader) { printf("Error\n"); } virtual void NotifySliceImageReady(const OrthancSliceLoader& loader, unsigned int sliceIndex, const Slice& slice, Orthanc::ImageAccessor* image) { std::auto_ptr<Orthanc::ImageAccessor> tmp(image); printf("Slice OK\n"); } virtual void NotifySliceImageError(const OrthancSliceLoader& loader, unsigned int sliceIndex, const Slice& slice) { printf("ERROR 2\n"); } }; } TEST(Toto, Tutu) { Orthanc::WebServiceParameters web; OrthancStone::OrthancSynchronousWebService orthanc(web); OrthancStone::Tata tata; OrthancStone::OrthancSliceLoader loader(tata, orthanc); //loader.ScheduleLoadSeries("c1c4cb95-05e3bd11-8da9f5bb-87278f71-0b2b43f5"); loader.ScheduleLoadSeries("67f1b334-02c16752-45026e40-a5b60b6b-030ecab5"); printf(">> %d\n", loader.GetSliceCount()); loader.ScheduleLoadSliceImage(31); } int main(int argc, char **argv) { Orthanc::Logging::Initialize(); Orthanc::Logging::EnableInfoLevel(true); ::testing::InitGoogleTest(&argc, argv); int result = RUN_ALL_TESTS(); Orthanc::Logging::Finalize(); return result; }