# HG changeset patch # User Sebastien Jodogne # Date 1495468166 -7200 # Node ID 1526d38ef6dad6e1c6487db9634f9f00aea8b310 # Parent acb60cbb83018b25a256e9e137b7f6f2a4a3fa57 SliceSorter diff -r acb60cbb8301 -r 1526d38ef6da UnitTestsSources/UnitTestsMain.cpp --- a/UnitTestsSources/UnitTestsMain.cpp Thu May 18 17:51:41 2017 +0200 +++ b/UnitTestsSources/UnitTestsMain.cpp Mon May 22 17:49:26 2017 +0200 @@ -27,12 +27,368 @@ #include "../Framework/Widgets/LayerWidget.h" +#include "../Resources/Orthanc/Core/Images/PngReader.h" #include "../Framework/Toolbox/MessagingToolbox.h" #include "../Framework/Toolbox/DicomFrameConverter.h" +#include + namespace OrthancStone { - class SeriesLoader : + 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::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 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 normalCandidates; + std::vector 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: @@ -43,246 +399,41 @@ { } - virtual void NotifyGeometryReady(const SeriesLoader& series) = 0; + virtual void NotifyGeometryReady(const OrthancSliceLoader& loader) = 0; + + virtual void NotifyGeometryError(const OrthancSliceLoader& loader) = 0; - virtual void NotifyGeometryError(const SeriesLoader& series) = 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: - class Slice : public boost::noncopyable - { - private: - std::string instanceId_; - SliceGeometry geometry_; - double thickness_; - unsigned int width_; - unsigned int height_; - double projectionAlongNormal_; - DicomFrameConverter converter_; - - public: - Slice(const std::string& instanceId, - const std::string& imagePositionPatient, - const std::string& imageOrientationPatient, - double thickness, - unsigned int width, - unsigned int height, - const OrthancPlugins::IDicomDataset& dataset) : - instanceId_(instanceId), - geometry_(imagePositionPatient, imageOrientationPatient), - thickness_(thickness), - width_(width), - height_(height) - { - converter_.ReadParameters(dataset); - } - - const std::string GetInstanceId() const - { - return instanceId_; - } - - const SliceGeometry& GetGeometry() const - { - return geometry_; - } - - double GetThickness() const - { - return thickness_; - } - - unsigned int GetWidth() const - { - return width_; - } - - unsigned int GetHeight() const - { - return height_; - } - - const DicomFrameConverter& GetConverter() const - { - return converter_; - } - - void SetNormal(const Vector& normal) - { - projectionAlongNormal_ = boost::numeric::ublas::inner_prod(geometry_.GetOrigin(), normal); - } - - double GetProjectionAlongNormal() const - { - return projectionAlongNormal_; - } - }; - - - class SetOfSlices : public boost::noncopyable + enum State { - private: - std::vector 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 Json::Value& value) - { - OrthancPlugins::FullOrthancDataset dataset(value); - OrthancPlugins::DicomDatasetReader reader(dataset); - - std::string position, orientation; - double thickness; - unsigned int width, height; - if (dataset.GetStringValue(position, OrthancPlugins::DICOM_TAG_IMAGE_POSITION_PATIENT) && - dataset.GetStringValue(orientation, OrthancPlugins::DICOM_TAG_IMAGE_ORIENTATION_PATIENT) && - reader.GetDoubleValue(thickness, OrthancPlugins::DICOM_TAG_SLICE_THICKNESS) && - reader.GetUnsignedIntegerValue(width, OrthancPlugins::DICOM_TAG_COLUMNS) && - reader.GetUnsignedIntegerValue(height, OrthancPlugins::DICOM_TAG_ROWS)); - { - slices_.push_back(new Slice(instanceId, position, orientation, - thickness, width, height, dataset)); - } - } - - 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 SelectNormal(Vector& normal) const - { - std::vector normalCandidates; - std::vector 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); - } + State_Error, + State_Initialization, + State_LoadingGeometry, + State_GeometryReady }; - enum Mode { - Mode_Geometry + Mode_SeriesGeometry, + Mode_LoadImage }; class Operation : public Orthanc::IDynamicObject { private: - Mode mode_; - unsigned int instance_; + Mode mode_; + unsigned int sliceIndex_; + const Slice* slice_; Operation(Mode mode) : mode_(mode) @@ -294,21 +445,42 @@ { 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* DownloadGeometry() + static Operation* DownloadSeriesGeometry() { - return new Operation(Mode_Geometry); + return new Operation(Mode_SeriesGeometry); + } + + static Operation* DownloadSliceImage(unsigned int sliceIndex, + const Slice& slice) + { + std::auto_ptr tmp(new Operation(Mode_LoadImage)); + tmp->sliceIndex_ = sliceIndex; + tmp->slice_ = &slice; + return tmp.release(); } }; ICallback& callback_; IWebService& orthanc_; - std::string seriesId_; - SetOfSlices slices_; + State state_; + SliceSorter slices_; - void ParseGeometry(const void* answer, - size_t size) + void ParseSeriesGeometry(const void* answer, + size_t size) { Json::Value series; if (!MessagingToolbox::ParseJson(series, answer, size) || @@ -324,7 +496,17 @@ for (size_t i = 0; i < instances.size(); i++) { - slices_.AddSlice(instances[i], series[instances[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; @@ -332,41 +514,141 @@ if (slices_.GetSliceCount() > 0) { Vector normal; - slices_.SelectNormal(normal); - slices_.FilterNormal(normal); - slices_.Sort(normal); - ok = true; + if (slices_.SelectNormal(normal)) + { + slices_.FilterNormal(normal); + slices_.SetNormal(normal); + slices_.Sort(); + ok = true; + } } if (ok) { - printf("%d\n", slices_.GetSliceCount()); + LOG(INFO) << "Loaded a series with " << slices_.GetSliceCount() << " slice(s)"; callback_.NotifyGeometryReady(*this); } else { LOG(ERROR) << "This series is empty"; callback_.NotifyGeometryError(*this); - return; + } + } + + + void ParseSliceImage(const Operation& operation, + const void* answer, + size_t size) + { + std::auto_ptr 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: - SeriesLoader(ICallback& callback, - IWebService& orthanc, - const std::string& seriesId) : + OrthancSliceLoader(ICallback& callback, + IWebService& orthanc) : callback_(callback), orthanc_(orthanc), - seriesId_(seriesId) + state_(State_Initialization) + { + } + + void ScheduleLoadSeries(const std::string& seriesId) { - std::string uri = "/series/" + seriesId + "/instances-tags"; - orthanc.ScheduleGetRequest(*this, uri, Operation::DownloadGeometry()); + 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 std::string& GetSeriesId() const + 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) { - return seriesId_; + 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(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, @@ -378,8 +660,13 @@ switch (operation->GetMode()) { - case Mode_Geometry: - ParseGeometry(answer, answerSize); + case Mode_SeriesGeometry: + ParseSeriesGeometry(answer, answerSize); + state_ = State_GeometryReady; + break; + + case Mode_LoadImage: + ParseSliceImage(*operation, answer, answerSize); break; default: @@ -395,10 +682,15 @@ switch (operation->GetMode()) { - case Mode_Geometry: + 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); } @@ -406,17 +698,33 @@ }; - class Tata : public SeriesLoader::ICallback + class Tata : public OrthancSliceLoader::ICallback { public: - virtual void NotifyGeometryReady(const SeriesLoader& series) + virtual void NotifyGeometryReady(const OrthancSliceLoader& loader) { - printf("Done %s\n", series.GetSeriesId().c_str()); + printf("Done\n"); + } + + virtual void NotifyGeometryError(const OrthancSliceLoader& loader) + { + printf("Error\n"); } - virtual void NotifyGeometryError(const SeriesLoader& series) + virtual void NotifySliceImageReady(const OrthancSliceLoader& loader, + unsigned int sliceIndex, + const Slice& slice, + Orthanc::ImageAccessor* image) { - printf("Error %s\n", series.GetSeriesId().c_str()); + std::auto_ptr tmp(image); + printf("Slice OK\n"); + } + + virtual void NotifySliceImageError(const OrthancSliceLoader& loader, + unsigned int sliceIndex, + const Slice& slice) + { + printf("ERROR 2\n"); } }; } @@ -428,8 +736,12 @@ OrthancStone::OrthancWebService orthanc(web); OrthancStone::Tata tata; - //OrthancStone::SeriesLoader loader(tata, orthanc, "c1c4cb95-05e3bd11-8da9f5bb-87278f71-0b2b43f5"); - OrthancStone::SeriesLoader loader(tata, orthanc, "67f1b334-02c16752-45026e40-a5b60b6b-030ecab5"); + 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); }