Mercurial > hg > orthanc-stone
changeset 73:ffa6dded91bd wasm
reorganization
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Wed, 24 May 2017 11:59:24 +0200 |
parents | c1cc3bdba18c |
children | 6546dbcc0a7d |
files | Framework/Toolbox/OrthancSlicesLoader.cpp Framework/Toolbox/OrthancSlicesLoader.h Framework/Toolbox/Slice.cpp Framework/Toolbox/Slice.h Framework/Toolbox/SlicesSorter.cpp Framework/Toolbox/SlicesSorter.h Resources/CMake/OrthancStone.cmake UnitTestsSources/UnitTestsMain.cpp |
diffstat | 8 files changed, 1026 insertions(+), 768 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Toolbox/OrthancSlicesLoader.cpp Wed May 24 11:59:24 2017 +0200 @@ -0,0 +1,394 @@ +/** + * 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 "OrthancSlicesLoader.h" + +#include "MessagingToolbox.h" + +#include "../../Resources/Orthanc/Core/Images/PngReader.h" +#include "../../Resources/Orthanc/Core/Logging.h" +#include "../../Resources/Orthanc/Core/OrthancException.h" +#include "../../Resources/Orthanc/Plugins/Samples/Common/DicomDatasetReader.h" +#include "../../Resources/Orthanc/Plugins/Samples/Common/FullOrthancDataset.h" + +#include <boost/lexical_cast.hpp> + +namespace OrthancStone +{ + class OrthancSlicesLoader::Operation : public Orthanc::IDynamicObject + { + private: + Mode mode_; + unsigned int frame_; + unsigned int sliceIndex_; + const Slice* slice_; + std::string instanceId_; + + 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_; + } + + unsigned int GetFrame() const + { + assert(mode_ == Mode_InstanceGeometry); + return frame_; + } + + const std::string& GetInstanceId() const + { + assert(mode_ == Mode_InstanceGeometry); + return instanceId_; + } + + static Operation* DownloadSeriesGeometry() + { + return new Operation(Mode_SeriesGeometry); + } + + static Operation* DownloadInstanceGeometry(const std::string& instanceId, + unsigned int frame) + { + std::auto_ptr<Operation> operation(new Operation(Mode_InstanceGeometry)); + operation->instanceId_ = instanceId; + operation->frame_ = frame; + return operation.release(); + } + + 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(); + } + }; + + + class OrthancSlicesLoader::WebCallback : public IWebService::ICallback + { + private: + OrthancSlicesLoader& that_; + + public: + WebCallback(OrthancSlicesLoader& that) : + that_(that) + { + } + + 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: + that_.ParseSeriesGeometry(answer, answerSize); + break; + + case Mode_InstanceGeometry: + that_.ParseInstanceGeometry(operation->GetInstanceId(), + operation->GetFrame(), answer, answerSize); + break; + + case Mode_LoadImage: + that_.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: + that_.userCallback_.NotifyGeometryError(that_); + that_.state_ = State_Error; + break; + + case Mode_LoadImage: + that_.userCallback_.NotifySliceImageError(that_, operation->GetSliceIndex(), + operation->GetSlice()); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + }; + + + void OrthancSlicesLoader::ParseSeriesGeometry(const void* answer, + size_t size) + { + Json::Value series; + if (!MessagingToolbox::ParseJson(series, answer, size) || + series.type() != Json::objectValue) + { + userCallback_.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; + } + } + + state_ = State_GeometryReady; + + if (ok) + { + LOG(INFO) << "Loaded a series with " << slices_.GetSliceCount() << " slice(s)"; + userCallback_.NotifyGeometryReady(*this); + } + else + { + LOG(ERROR) << "This series is empty"; + userCallback_.NotifyGeometryError(*this); + } + } + + + void OrthancSlicesLoader::ParseInstanceGeometry(const std::string& instanceId, + unsigned int frame, + const void* answer, + size_t size) + { + Json::Value tags; + if (!MessagingToolbox::ParseJson(tags, answer, size) || + tags.type() != Json::objectValue) + { + userCallback_.NotifyGeometryError(*this); + return; + } + + OrthancPlugins::FullOrthancDataset dataset(tags); + + state_ = State_GeometryReady; + + Slice slice; + if (slice.ParseOrthancFrame(dataset, instanceId, frame)) + { + LOG(INFO) << "Loaded instance " << instanceId; + slices_.AddSlice(slice); + userCallback_.NotifyGeometryReady(*this); + } + else + { + LOG(WARNING) << "Skipping invalid instance " << instanceId; + userCallback_.NotifyGeometryError(*this); + } + } + + + void OrthancSlicesLoader::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) + { + userCallback_.NotifySliceImageReady(*this, operation.GetSliceIndex(), + operation.GetSlice(), image.release()); + } + else + { + userCallback_.NotifySliceImageError(*this, operation.GetSliceIndex(), + operation.GetSlice()); + } + } + + + OrthancSlicesLoader::OrthancSlicesLoader(ICallback& callback, + IWebService& orthanc) : + webCallback_(new WebCallback(*this)), + userCallback_(callback), + orthanc_(orthanc), + state_(State_Initialization) + { + } + + + void OrthancSlicesLoader::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(*webCallback_, uri, Operation::DownloadSeriesGeometry()); + } + } + + + void OrthancSlicesLoader::ScheduleLoadInstance(const std::string& instanceId, + unsigned int frame) + { + if (state_ != State_Initialization) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + state_ = State_LoadingGeometry; + std::string uri = "/instances/" + instanceId + "/tags"; + orthanc_.ScheduleGetRequest + (*webCallback_, uri, Operation::DownloadInstanceGeometry(instanceId, frame)); + } + } + + + size_t OrthancSlicesLoader::GetSliceCount() const + { + if (state_ != State_GeometryReady) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + return slices_.GetSliceCount(); + } + + + const Slice& OrthancSlicesLoader::GetSlice(size_t index) const + { + if (state_ != State_GeometryReady) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + return slices_.GetSlice(index); + } + + + void OrthancSlicesLoader::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(*webCallback_, uri, Operation::DownloadSliceImage(index, slice)); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Toolbox/OrthancSlicesLoader.h Wed May 24 11:59:24 2017 +0200 @@ -0,0 +1,110 @@ +/** + * 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/>. + **/ + + +#pragma once + +#include "IWebService.h" +#include "SlicesSorter.h" + +#include <boost/shared_ptr.hpp> + +namespace OrthancStone +{ + class OrthancSlicesLoader : public boost::noncopyable + { + public: + class ICallback : public boost::noncopyable + { + public: + virtual ~ICallback() + { + } + + virtual void NotifyGeometryReady(const OrthancSlicesLoader& loader) = 0; + + virtual void NotifyGeometryError(const OrthancSlicesLoader& loader) = 0; + + virtual void NotifySliceImageReady(const OrthancSlicesLoader& loader, + unsigned int sliceIndex, + const Slice& slice, + Orthanc::ImageAccessor* image) = 0; + + virtual void NotifySliceImageError(const OrthancSlicesLoader& loader, + unsigned int sliceIndex, + const Slice& slice) = 0; + }; + + private: + enum State + { + State_Error, + State_Initialization, + State_LoadingGeometry, + State_GeometryReady + }; + + enum Mode + { + Mode_SeriesGeometry, + Mode_InstanceGeometry, + Mode_LoadImage + }; + + class Operation; + class WebCallback; + + boost::shared_ptr<WebCallback> webCallback_; // This is a PImpl pattern + + ICallback& userCallback_; + IWebService& orthanc_; + State state_; + SlicesSorter slices_; + + + void ParseSeriesGeometry(const void* answer, + size_t size); + + void ParseInstanceGeometry(const std::string& instanceId, + unsigned int frame, + const void* answer, + size_t size); + + void ParseSliceImage(const Operation& operation, + const void* answer, + size_t size); + + + public: + OrthancSlicesLoader(ICallback& callback, + IWebService& orthanc); + + void ScheduleLoadSeries(const std::string& seriesId); + + void ScheduleLoadInstance(const std::string& instanceId, + unsigned int frame); + + size_t GetSliceCount() const; + + const Slice& GetSlice(size_t index) const; + + void ScheduleLoadSliceImage(size_t index); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Toolbox/Slice.cpp Wed May 24 11:59:24 2017 +0200 @@ -0,0 +1,173 @@ +/** + * 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 "Slice.h" + +#include "../../Resources/Orthanc/Core/OrthancException.h" + +namespace OrthancStone +{ + bool Slice::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)) + { + geometry_ = SliceGeometry(position, orientation); + } + + if (reader.GetUnsignedIntegerValue(width_, OrthancPlugins::DICOM_TAG_COLUMNS) && + reader.GetUnsignedIntegerValue(height_, OrthancPlugins::DICOM_TAG_ROWS)) + { + orthancInstanceId_ = instanceId; + frame_ = frame; + converter_.ReadParameters(dataset); + + type_ = Type_OrthancInstance; + return true; + } + else + { + return false; + } + } + + + const std::string Slice::GetOrthancInstanceId() const + { + if (type_ != Type_OrthancInstance) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + return orthancInstanceId_; + } + + + unsigned int Slice::GetFrame() const + { + if (type_ == Type_Invalid) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + return frame_; + } + + + const SliceGeometry& Slice::GetGeometry() const + { + if (type_ == Type_Invalid) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + return geometry_; + } + + + double Slice::GetThickness() const + { + if (type_ == Type_Invalid) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + return thickness_; + } + + + double Slice::GetPixelSpacingX() const + { + if (type_ == Type_Invalid) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + return pixelSpacingX_; + } + + + double Slice::GetPixelSpacingY() const + { + if (type_ == Type_Invalid) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + return pixelSpacingY_; + } + + + unsigned int Slice::GetWidth() const + { + if (type_ == Type_Invalid) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + return width_; + } + + + unsigned int Slice::GetHeight() const + { + if (type_ == Type_Invalid) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + return height_; + } + + + const DicomFrameConverter& Slice::GetConverter() const + { + if (type_ == Type_Invalid) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + return converter_; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Toolbox/Slice.h Wed May 24 11:59:24 2017 +0200 @@ -0,0 +1,82 @@ +/** + * 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/>. + **/ + + +#pragma once + +#include "SliceGeometry.h" +#include "DicomFrameConverter.h" + +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); + + bool IsOrthancInstance() const + { + return type_ == Type_OrthancInstance; + } + + const std::string GetOrthancInstanceId() const; + + unsigned int GetFrame() const; + + const SliceGeometry& GetGeometry() const; + + double GetThickness() const; + + double GetPixelSpacingX() const; + + double GetPixelSpacingY() const; + + unsigned int GetWidth() const; + + unsigned int GetHeight() const; + + const DicomFrameConverter& GetConverter() const; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Toolbox/SlicesSorter.cpp Wed May 24 11:59:24 2017 +0200 @@ -0,0 +1,195 @@ +/** + * 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 "SlicesSorter.h" + +#include "../../Resources/Orthanc/Core/OrthancException.h" + +namespace OrthancStone +{ + class SlicesSorter::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 SlicesSorter::Comparator + { + bool operator() (const SliceWithDepth* const& a, + const SliceWithDepth* const& b) const + { + return a->GetDepth() < b->GetDepth(); + } + }; + + + SlicesSorter::~SlicesSorter() + { + for (size_t i = 0; i < slices_.size(); i++) + { + assert(slices_[i] != NULL); + delete slices_[i]; + } + } + + + void SlicesSorter::AddSlice(const Slice& slice) + { + slices_.push_back(new SliceWithDepth(slice)); + } + + + const Slice& SlicesSorter::GetSlice(size_t i) const + { + if (i >= slices_.size()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + assert(slices_[i] != NULL); + return slices_[i]->GetSlice(); + } + + + void SlicesSorter::SetNormal(const Vector& normal) + { + for (size_t i = 0; i < slices_.size(); i++) + { + slices_[i]->SetNormal(normal); + } + + hasNormal_ = true; + } + + + void SlicesSorter::Sort() + { + if (!hasNormal_) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + + Comparator comparator; + std::sort(slices_.begin(), slices_.end(), comparator); + } + + + void SlicesSorter::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 SlicesSorter::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; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Framework/Toolbox/SlicesSorter.h Wed May 24 11:59:24 2017 +0200 @@ -0,0 +1,68 @@ +/** + * 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/>. + **/ + + +#pragma once + +#include "Slice.h" + +namespace OrthancStone +{ + class SlicesSorter : public boost::noncopyable + { + private: + class SliceWithDepth; + struct Comparator; + + typedef std::vector<SliceWithDepth*> Slices; + + Slices slices_; + bool hasNormal_; + + public: + SlicesSorter() : hasNormal_(false) + { + } + + ~SlicesSorter(); + + void Reserve(size_t count) + { + slices_.reserve(count); + } + + void AddSlice(const Slice& slice); + + size_t GetSliceCount() const + { + return slices_.size(); + } + + const Slice& GetSlice(size_t i) const; + + void SetNormal(const Vector& normal); + + void Sort(); + + void FilterNormal(const Vector& normal); + + bool SelectNormal(Vector& normal) const; + }; +}
--- a/Resources/CMake/OrthancStone.cmake Wed May 24 10:36:41 2017 +0200 +++ b/Resources/CMake/OrthancStone.cmake Wed May 24 11:59:24 2017 +0200 @@ -202,10 +202,13 @@ ${ORTHANC_STONE_DIR}/Framework/Toolbox/MessagingToolbox.cpp ${ORTHANC_STONE_DIR}/Framework/Toolbox/OrthancAsynchronousWebService.cpp ${ORTHANC_STONE_DIR}/Framework/Toolbox/OrthancSeriesLoader.cpp + ${ORTHANC_STONE_DIR}/Framework/Toolbox/OrthancSlicesLoader.cpp ${ORTHANC_STONE_DIR}/Framework/Toolbox/OrthancSynchronousWebService.cpp ${ORTHANC_STONE_DIR}/Framework/Toolbox/ParallelSlices.cpp ${ORTHANC_STONE_DIR}/Framework/Toolbox/ParallelSlicesCursor.cpp + ${ORTHANC_STONE_DIR}/Framework/Toolbox/Slice.cpp ${ORTHANC_STONE_DIR}/Framework/Toolbox/SliceGeometry.cpp + ${ORTHANC_STONE_DIR}/Framework/Toolbox/SlicesSorter.cpp ${ORTHANC_STONE_DIR}/Framework/Toolbox/ViewportGeometry.cpp ${ORTHANC_STONE_DIR}/Framework/Viewport/CairoContext.cpp ${ORTHANC_STONE_DIR}/Framework/Viewport/CairoFont.cpp
--- a/UnitTestsSources/UnitTestsMain.cpp Wed May 24 10:36:41 2017 +0200 +++ b/UnitTestsSources/UnitTestsMain.cpp Wed May 24 11:59:24 2017 +0200 @@ -22,15 +22,8 @@ #include "gtest/gtest.h" #include "../Framework/Toolbox/OrthancAsynchronousWebService.h" +#include "../Framework/Toolbox/OrthancSlicesLoader.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> #include <boost/date_time/posix_time/posix_time.hpp> @@ -38,766 +31,6 @@ 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)) - { - geometry_ = SliceGeometry(position, orientation); - } - - if (reader.GetUnsignedIntegerValue(width_, OrthancPlugins::DICOM_TAG_COLUMNS) && - reader.GetUnsignedIntegerValue(height_, OrthancPlugins::DICOM_TAG_ROWS)) - { - orthancInstanceId_ = instanceId; - frame_ = frame; - 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 OrthancSlicesLoader : public boost::noncopyable - { - public: - class ICallback : public boost::noncopyable - { - public: - virtual ~ICallback() - { - } - - virtual void NotifyGeometryReady(const OrthancSlicesLoader& loader) = 0; - - virtual void NotifyGeometryError(const OrthancSlicesLoader& loader) = 0; - - virtual void NotifySliceImageReady(const OrthancSlicesLoader& loader, - unsigned int sliceIndex, - const Slice& slice, - Orthanc::ImageAccessor* image) = 0; - - virtual void NotifySliceImageError(const OrthancSlicesLoader& loader, - unsigned int sliceIndex, - const Slice& slice) = 0; - }; - - private: - enum State - { - State_Error, - State_Initialization, - State_LoadingGeometry, - State_GeometryReady - }; - - enum Mode - { - Mode_SeriesGeometry, - Mode_InstanceGeometry, - Mode_LoadImage - }; - - class Operation : public Orthanc::IDynamicObject - { - private: - Mode mode_; - unsigned int frame_; - unsigned int sliceIndex_; - const Slice* slice_; - std::string instanceId_; - - 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_; - } - - unsigned int GetFrame() const - { - assert(mode_ == Mode_InstanceGeometry); - return frame_; - } - - const std::string& GetInstanceId() const - { - assert(mode_ == Mode_InstanceGeometry); - return instanceId_; - } - - static Operation* DownloadSeriesGeometry() - { - return new Operation(Mode_SeriesGeometry); - } - - static Operation* DownloadInstanceGeometry(const std::string& instanceId, - unsigned int frame) - { - std::auto_ptr<Operation> operation(new Operation(Mode_InstanceGeometry)); - operation->instanceId_ = instanceId; - operation->frame_ = frame; - return operation.release(); - } - - 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(); - } - }; - - - class WebCallback : public IWebService::ICallback - { - private: - OrthancSlicesLoader& that_; - - public: - WebCallback(OrthancSlicesLoader& that) : - that_(that) - { - } - - 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: - that_.ParseSeriesGeometry(answer, answerSize); - break; - - case Mode_InstanceGeometry: - that_.ParseInstanceGeometry(operation->GetInstanceId(), - operation->GetFrame(), answer, answerSize); - break; - - case Mode_LoadImage: - that_.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: - that_.userCallback_.NotifyGeometryError(that_); - that_.state_ = State_Error; - break; - - case Mode_LoadImage: - that_.userCallback_.NotifySliceImageError(that_, operation->GetSliceIndex(), - operation->GetSlice()); - break; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - } - }; - - - WebCallback webCallback_; - ICallback& userCallback_; - 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) - { - userCallback_.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; - } - } - - state_ = State_GeometryReady; - - if (ok) - { - LOG(INFO) << "Loaded a series with " << slices_.GetSliceCount() << " slice(s)"; - userCallback_.NotifyGeometryReady(*this); - } - else - { - LOG(ERROR) << "This series is empty"; - userCallback_.NotifyGeometryError(*this); - } - } - - - void ParseInstanceGeometry(const std::string& instanceId, - unsigned int frame, - const void* answer, - size_t size) - { - Json::Value tags; - if (!MessagingToolbox::ParseJson(tags, answer, size) || - tags.type() != Json::objectValue) - { - userCallback_.NotifyGeometryError(*this); - return; - } - - OrthancPlugins::FullOrthancDataset dataset(tags); - - state_ = State_GeometryReady; - - Slice slice; - if (slice.ParseOrthancFrame(dataset, instanceId, frame)) - { - LOG(INFO) << "Loaded instance " << instanceId; - slices_.AddSlice(slice); - userCallback_.NotifyGeometryReady(*this); - } - else - { - LOG(WARNING) << "Skipping invalid instance " << instanceId; - userCallback_.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) - { - userCallback_.NotifySliceImageReady(*this, operation.GetSliceIndex(), - operation.GetSlice(), image.release()); - } - else - { - userCallback_.NotifySliceImageError(*this, operation.GetSliceIndex(), - operation.GetSlice()); - } - } - - - public: - OrthancSlicesLoader(ICallback& callback, - IWebService& orthanc) : - webCallback_(*this), - userCallback_(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(webCallback_, uri, Operation::DownloadSeriesGeometry()); - } - } - - void ScheduleLoadInstance(const std::string& instanceId, - unsigned int frame) - { - if (state_ != State_Initialization) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); - } - else - { - state_ = State_LoadingGeometry; - std::string uri = "/instances/" + instanceId + "/tags"; - orthanc_.ScheduleGetRequest - (webCallback_, uri, Operation::DownloadInstanceGeometry(instanceId, frame)); - } - } - - 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(webCallback_, uri, Operation::DownloadSliceImage(index, slice)); - } - } - }; - - class Tata : public OrthancSlicesLoader::ICallback { public: