Mercurial > hg > orthanc-stone
changeset 652:6646957eff7f
Merge from default
author | Benjamin Golinvaux <bgo@osimis.io> |
---|---|
date | Tue, 14 May 2019 09:49:24 +0200 |
parents | 62f6ff016085 (current diff) 1088d4c4d78c (diff) |
children | 4eccf698e52f |
files | |
diffstat | 13 files changed, 228 insertions(+), 112 deletions(-) [+] |
line wrap: on
line diff
--- a/Applications/Samples/SingleFrameApplication.h Tue May 14 09:48:14 2019 +0200 +++ b/Applications/Samples/SingleFrameApplication.h Tue May 14 09:49:24 2019 +0200 @@ -136,9 +136,9 @@ slice = 0; } - if (slice >= static_cast<int>(source_->GetSliceCount())) + if (slice >= static_cast<int>(source_->GetSlicesCount())) { - slice = static_cast<int>(source_->GetSliceCount()) - 1; + slice = static_cast<int>(source_->GetSlicesCount()) - 1; } if (slice != static_cast<int>(slice_)) @@ -158,7 +158,7 @@ void SetSlice(size_t index) { if (source_ != NULL && - index < source_->GetSliceCount()) + index < source_->GetSlicesCount()) { slice_ = static_cast<unsigned int>(index); @@ -191,7 +191,7 @@ // slice if (source_ == &message.GetOrigin()) { - SetSlice(source_->GetSliceCount() / 2); + SetSlice(source_->GetSlicesCount() / 2); } GetMainWidget().FitContent();
--- a/Framework/Layers/DicomSeriesVolumeSlicer.cpp Tue May 14 09:48:14 2019 +0200 +++ b/Framework/Layers/DicomSeriesVolumeSlicer.cpp Tue May 14 09:49:24 2019 +0200 @@ -34,7 +34,7 @@ void DicomSeriesVolumeSlicer::OnSliceGeometryReady(const OrthancSlicesLoader::SliceGeometryReadyMessage& message) { - if (message.GetOrigin().GetSliceCount() > 0) + if (message.GetOrigin().GetSlicesCount() > 0) { BroadcastMessage(IVolumeSlicer::GeometryReadyMessage(*this)); }
--- a/Framework/Layers/DicomSeriesVolumeSlicer.h Tue May 14 09:48:14 2019 +0200 +++ b/Framework/Layers/DicomSeriesVolumeSlicer.h Tue May 14 09:49:24 2019 +0200 @@ -102,9 +102,9 @@ return quality_; } - size_t GetSliceCount() const + size_t GetSlicesCount() const { - return loader_.GetSliceCount(); + return loader_.GetSlicesCount(); } const Slice& GetSlice(size_t slice) const
--- a/Framework/Toolbox/CoordinateSystem3D.cpp Tue May 14 09:48:14 2019 +0200 +++ b/Framework/Toolbox/CoordinateSystem3D.cpp Tue May 14 09:49:24 2019 +0200 @@ -187,4 +187,24 @@ { return GeometryToolbox::IntersectPlaneAndLine(p, normal_, d_, origin, direction); } + + + bool CoordinateSystem3D::GetDistance(double& distance, + const CoordinateSystem3D& a, + const CoordinateSystem3D& b) + { + bool opposite; // Ignored + + if (OrthancStone::GeometryToolbox::IsParallelOrOpposite( + opposite, a.GetNormal(), b.GetNormal())) + { + distance = std::abs(a.ProjectAlongNormal(a.GetOrigin()) - + a.ProjectAlongNormal(b.GetOrigin())); + return true; + } + else + { + return false; + } + } }
--- a/Framework/Toolbox/CoordinateSystem3D.h Tue May 14 09:48:14 2019 +0200 +++ b/Framework/Toolbox/CoordinateSystem3D.h Tue May 14 09:49:24 2019 +0200 @@ -102,5 +102,10 @@ bool IntersectLine(Vector& p, const Vector& origin, const Vector& direction) const; + + // Returns "false" is the two planes are not parallel + static bool GetDistance(double& distance, + const CoordinateSystem3D& a, + const CoordinateSystem3D& b); }; }
--- a/Framework/Toolbox/LinearAlgebra.h Tue May 14 09:48:14 2019 +0200 +++ b/Framework/Toolbox/LinearAlgebra.h Tue May 14 09:49:24 2019 +0200 @@ -137,7 +137,7 @@ double y, double threshold) { - return fabs(x - y) < threshold; + return fabs(x - y) <= threshold; } inline bool IsNear(double x,
--- a/Framework/Toolbox/OrthancSlicesLoader.cpp Tue May 14 09:48:14 2019 +0200 +++ b/Framework/Toolbox/OrthancSlicesLoader.cpp Tue May 14 09:49:24 2019 +0200 @@ -191,25 +191,13 @@ void OrthancSlicesLoader::SortAndFinalizeSlices() { - bool ok = false; - - if (slices_.GetSliceCount() > 0) - { - Vector normal; - if (slices_.SelectNormal(normal)) - { - slices_.FilterNormal(normal); - slices_.SetNormal(normal); - slices_.Sort(); - ok = true; - } - } + bool ok = slices_.Sort(); state_ = State_GeometryReady; if (ok) { - LOG(INFO) << "Loaded a series with " << slices_.GetSliceCount() << " slice(s)"; + LOG(INFO) << "Loaded a series with " << slices_.GetSlicesCount() << " slice(s)"; BroadcastMessage(SliceGeometryReadyMessage(*this)); } else @@ -256,7 +244,8 @@ std::auto_ptr<Slice> slice(new Slice); if (slice->ParseOrthancFrame(dicom, instances[i], frame)) { - slices_.AddSlice(slice.release()); + CoordinateSystem3D geometry = slice->GetGeometry(); + slices_.AddSlice(geometry, slice.release()); } else { @@ -291,7 +280,8 @@ std::auto_ptr<Slice> slice(new Slice); if (slice->ParseOrthancFrame(dicom, instanceId, frame)) { - slices_.AddSlice(slice.release()); + CoordinateSystem3D geometry = slice->GetGeometry(); + slices_.AddSlice(geometry, slice.release()); } else { @@ -322,7 +312,10 @@ if (slice->ParseOrthancFrame(dicom, instanceId, frame)) { LOG(INFO) << "Loaded instance geometry " << instanceId; - slices_.AddSlice(slice.release()); + + CoordinateSystem3D geometry = slice->GetGeometry(); + slices_.AddSlice(geometry, slice.release()); + BroadcastMessage(SliceGeometryReadyMessage(*this)); } else @@ -717,14 +710,14 @@ } - size_t OrthancSlicesLoader::GetSliceCount() const + size_t OrthancSlicesLoader::GetSlicesCount() const { if (state_ != State_GeometryReady) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } - return slices_.GetSliceCount(); + return slices_.GetSlicesCount(); } @@ -734,8 +727,8 @@ { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } - - return slices_.GetSlice(index); + + return dynamic_cast<const Slice&>(slices_.GetSlicePayload(index)); } @@ -746,8 +739,10 @@ { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } - - return slices_.LookupSlice(index, plane); + + double distance; + return (slices_.LookupClosestSlice(index, distance, plane) && + distance <= GetSlice(index).GetThickness() / 2.0); }
--- a/Framework/Toolbox/OrthancSlicesLoader.h Tue May 14 09:48:14 2019 +0200 +++ b/Framework/Toolbox/OrthancSlicesLoader.h Tue May 14 09:49:24 2019 +0200 @@ -26,6 +26,7 @@ #include "IWebService.h" #include "OrthancApiClient.h" #include "SlicesSorter.h" +#include "Slice.h" #include <Core/Images/Image.h> @@ -195,7 +196,7 @@ bool IsGeometryReady() const; - size_t GetSliceCount() const; + size_t GetSlicesCount() const; const Slice& GetSlice(size_t index) const;
--- a/Framework/Toolbox/Slice.h Tue May 14 09:48:14 2019 +0200 +++ b/Framework/Toolbox/Slice.h Tue May 14 09:49:24 2019 +0200 @@ -25,10 +25,13 @@ #include "DicomFrameConverter.h" #include <Core/DicomFormat/DicomImageInformation.h> +#include <Core/IDynamicObject.h> namespace OrthancStone { - class Slice : public boost::noncopyable + // TODO - Remove this class + class Slice : + public Orthanc::IDynamicObject /* to be used as a payload of SlicesSorter */ { private: enum Type
--- a/Framework/Toolbox/SlicesSorter.cpp Tue May 14 09:48:14 2019 +0200 +++ b/Framework/Toolbox/SlicesSorter.cpp Tue May 14 09:49:24 2019 +0200 @@ -30,25 +30,23 @@ class SlicesSorter::SliceWithDepth : public boost::noncopyable { private: - std::auto_ptr<Slice> slice_; - double depth_; + CoordinateSystem3D geometry_; + double depth_; + + std::auto_ptr<Orthanc::IDynamicObject> payload_; public: - SliceWithDepth(Slice* slice) : - slice_(slice), - depth_(0) + SliceWithDepth(const CoordinateSystem3D& geometry, + Orthanc::IDynamicObject* payload) : + geometry_(geometry), + depth_(0), + payload_(payload) { - if (slice == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } } void SetNormal(const Vector& normal) { - assert(slice_.get() != NULL); - depth_ = boost::numeric::ublas::inner_prod - (slice_->GetGeometry().GetOrigin(), normal); + depth_ = boost::numeric::ublas::inner_prod(geometry_.GetOrigin(), normal); } double GetDepth() const @@ -56,10 +54,26 @@ return depth_; } - const Slice& GetSlice() const + const CoordinateSystem3D& GetGeometry() const + { + return geometry_; + } + + bool HasPayload() const { - assert(slice_.get() != NULL); - return *slice_; + return (payload_.get() != NULL); + } + + const Orthanc::IDynamicObject& GetPayload() const + { + if (HasPayload()) + { + return *payload_; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } } }; @@ -84,21 +98,42 @@ } - void SlicesSorter::AddSlice(Slice* slice) + void SlicesSorter::AddSlice(const CoordinateSystem3D& slice, + Orthanc::IDynamicObject* payload) { - slices_.push_back(new SliceWithDepth(slice)); + slices_.push_back(new SliceWithDepth(slice, payload)); } - const Slice& SlicesSorter::GetSlice(size_t i) const + const SlicesSorter::SliceWithDepth& SlicesSorter::GetSlice(size_t i) const { if (i >= slices_.size()) { throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } + else + { + assert(slices_[i] != NULL); + return *slices_[i]; + } + } - assert(slices_[i] != NULL); - return slices_[i]->GetSlice(); + + const CoordinateSystem3D& SlicesSorter::GetSliceGeometry(size_t i) const + { + return GetSlice(i).GetGeometry(); + } + + + bool SlicesSorter::HasSlicePayload(size_t i) const + { + return GetSlice(i).HasPayload(); + } + + + const Orthanc::IDynamicObject& SlicesSorter::GetSlicePayload(size_t i) const + { + return GetSlice(i).GetPayload(); } @@ -113,7 +148,7 @@ } - void SlicesSorter::Sort() + void SlicesSorter::SortInternal() { if (!hasNormal_) { @@ -131,7 +166,7 @@ for (size_t i = 0; i < slices_.size(); i++) { - if (GeometryToolbox::IsParallel(normal, slices_[i]->GetSlice().GetGeometry().GetNormal())) + if (GeometryToolbox::IsParallel(normal, slices_[i]->GetGeometry().GetNormal())) { // This slice is compatible with the selected normal slices_[pos] = slices_[i]; @@ -155,7 +190,7 @@ bool found = false; - for (size_t i = 0; !found && i < GetSliceCount(); i++) + for (size_t i = 0; !found && i < GetSlicesCount(); i++) { const Vector& normal = GetSlice(i).GetGeometry().GetNormal(); @@ -190,8 +225,8 @@ for (size_t i = 0; !found && i < normalCandidates.size(); i++) { unsigned int count = normalCount[i]; - if (count == GetSliceCount() || - count + 1 == GetSliceCount()) + if (count == GetSlicesCount() || + count + 1 == GetSlicesCount()) { normal = normalCandidates[i]; found = true; @@ -202,21 +237,52 @@ } - bool SlicesSorter::LookupSlice(size_t& index, - const CoordinateSystem3D& slice) const + bool SlicesSorter::Sort() { - // TODO Turn this linear-time lookup into a log-time lookup, - // keeping track of whether the slices are sorted along the normal - - for (size_t i = 0; i < slices_.size(); i++) + if (GetSlicesCount() > 0) { - if (slices_[i]->GetSlice().ContainsPlane(slice)) + Vector normal; + if (SelectNormal(normal)) { - index = i; + FilterNormal(normal); + SetNormal(normal); + SortInternal(); return true; } } return false; } + + + bool SlicesSorter::LookupClosestSlice(size_t& index, + double& distance, + const CoordinateSystem3D& slice) const + { + // TODO Turn this linear-time lookup into a log-time lookup, + // keeping track of whether the slices are sorted along the normal + + bool found = false; + + distance = std::numeric_limits<double>::infinity(); + + for (size_t i = 0; i < slices_.size(); i++) + { + assert(slices_[i] != NULL); + + double tmp; + if (CoordinateSystem3D::GetDistance(tmp, slices_[i]->GetGeometry(), slice)) + { + if (!found || + tmp < distance) + { + index = i; + distance = tmp; + found = true; + } + } + } + + return found; + } }
--- a/Framework/Toolbox/SlicesSorter.h Tue May 14 09:48:14 2019 +0200 +++ b/Framework/Toolbox/SlicesSorter.h Tue May 14 09:49:24 2019 +0200 @@ -21,10 +21,13 @@ #pragma once -#include "Slice.h" +#include "CoordinateSystem3D.h" + +#include <Core/IDynamicObject.h> namespace OrthancStone { + // TODO - Rename this as "PlanesSorter" class SlicesSorter : public boost::noncopyable { private: @@ -36,6 +39,16 @@ Slices slices_; bool hasNormal_; + const SliceWithDepth& GetSlice(size_t i) const; + + void SetNormal(const Vector& normal); + + void SortInternal(); + + void FilterNormal(const Vector& normal); + + bool SelectNormal(Vector& normal) const; + public: SlicesSorter() : hasNormal_(false) { @@ -48,24 +61,29 @@ slices_.reserve(count); } - void AddSlice(Slice* slice); // Takes ownership + void AddSlice(const CoordinateSystem3D& plane) + { + AddSlice(plane, NULL); + } - size_t GetSliceCount() const + void AddSlice(const CoordinateSystem3D& plane, + Orthanc::IDynamicObject* payload); // Takes ownership + + size_t GetSlicesCount() const { return slices_.size(); } - const Slice& GetSlice(size_t i) const; + const CoordinateSystem3D& GetSliceGeometry(size_t i) const; - void SetNormal(const Vector& normal); + bool HasSlicePayload(size_t i) const; - void Sort(); + const Orthanc::IDynamicObject& GetSlicePayload(size_t i) const; - void FilterNormal(const Vector& normal); + bool Sort(); - bool SelectNormal(Vector& normal) const; - - bool LookupSlice(size_t& index, - const CoordinateSystem3D& slice) const; + bool LookupClosestSlice(size_t& index, + double& distance, + const CoordinateSystem3D& slice) const; }; }
--- a/Framework/dev.h Tue May 14 09:48:14 2019 +0200 +++ b/Framework/dev.h Tue May 14 09:49:24 2019 +0200 @@ -110,14 +110,14 @@ { assert(&message.GetOrigin() == &loader_); - if (loader_.GetSliceCount() == 0) + if (loader_.GetSlicesCount() == 0) { LOG(ERROR) << "Empty volume image"; BroadcastMessage(ISlicedVolume::GeometryErrorMessage(*this)); return; } - for (size_t i = 1; i < loader_.GetSliceCount(); i++) + for (size_t i = 1; i < loader_.GetSlicesCount(); i++) { if (!IsCompatible(loader_.GetSlice(0), loader_.GetSlice(i))) { @@ -128,7 +128,7 @@ double spacingZ; - if (loader_.GetSliceCount() > 1) + if (loader_.GetSlicesCount() > 1) { spacingZ = GetDistance(loader_.GetSlice(0), loader_.GetSlice(1)); } @@ -139,7 +139,7 @@ spacingZ = 1; } - for (size_t i = 1; i < loader_.GetSliceCount(); i++) + for (size_t i = 1; i < loader_.GetSlicesCount(); i++) { if (!LinearAlgebra::IsNear(spacingZ, GetDistance(loader_.GetSlice(i - 1), loader_.GetSlice(i)), 0.001 /* this is expressed in mm */)) @@ -154,16 +154,16 @@ unsigned int height = loader_.GetSlice(0).GetHeight(); Orthanc::PixelFormat format = loader_.GetSlice(0).GetConverter().GetExpectedPixelFormat(); LOG(INFO) << "Creating a volume image of size " << width << "x" << height - << "x" << loader_.GetSliceCount() << " in " << Orthanc::EnumerationToString(format); + << "x" << loader_.GetSlicesCount() << " in " << Orthanc::EnumerationToString(format); - image_.reset(new ImageBuffer3D(format, width, height, static_cast<unsigned int>(loader_.GetSliceCount()), computeRange_)); + image_.reset(new ImageBuffer3D(format, width, height, static_cast<unsigned int>(loader_.GetSlicesCount()), computeRange_)); image_->SetAxialGeometry(loader_.GetSlice(0).GetGeometry()); image_->SetVoxelDimensions(loader_.GetSlice(0).GetPixelSpacingX(), loader_.GetSlice(0).GetPixelSpacingY(), spacingZ); image_->Clear(); - downloadStack_.reset(new DownloadStack(static_cast<unsigned int>(loader_.GetSliceCount()))); - pendingSlices_ = loader_.GetSliceCount(); + downloadStack_.reset(new DownloadStack(static_cast<unsigned int>(loader_.GetSlicesCount()))); + pendingSlices_ = loader_.GetSlicesCount(); for (unsigned int i = 0; i < 4; i++) // Limit to 4 simultaneous downloads { @@ -263,9 +263,9 @@ loader_.ScheduleLoadFrame(instanceId, frame); } - virtual size_t GetSliceCount() const + virtual size_t GetSlicesCount() const { - return loader_.GetSliceCount(); + return loader_.GetSlicesCount(); } virtual const Slice& GetSlice(size_t index) const @@ -317,7 +317,7 @@ { double thickness; - size_t n = volume.GetSliceCount(); + size_t n = volume.GetSlicesCount(); if (n > 1) { const Slice& a = volume.GetSlice(0); @@ -349,7 +349,7 @@ width_ = axial.GetWidth(); height_ = axial.GetHeight(); - depth_ = volume.GetSliceCount(); + depth_ = volume.GetSlicesCount(); pixelSpacingX_ = axial.GetPixelSpacingX(); pixelSpacingY_ = axial.GetPixelSpacingY(); @@ -364,7 +364,7 @@ double axialThickness = ComputeAxialThickness(volume); width_ = axial.GetWidth(); - height_ = static_cast<unsigned int>(volume.GetSliceCount()); + height_ = static_cast<unsigned int>(volume.GetSlicesCount()); depth_ = axial.GetHeight(); pixelSpacingX_ = axial.GetPixelSpacingX(); @@ -372,7 +372,7 @@ sliceThickness_ = axial.GetPixelSpacingY(); Vector origin = axial.GetGeometry().GetOrigin(); - origin += (static_cast<double>(volume.GetSliceCount() - 1) * + origin += (static_cast<double>(volume.GetSlicesCount() - 1) * axialThickness * axial.GetGeometry().GetNormal()); reference_ = CoordinateSystem3D(origin, @@ -386,7 +386,7 @@ double axialThickness = ComputeAxialThickness(volume); width_ = axial.GetHeight(); - height_ = static_cast<unsigned int>(volume.GetSliceCount()); + height_ = static_cast<unsigned int>(volume.GetSlicesCount()); depth_ = axial.GetWidth(); pixelSpacingX_ = axial.GetPixelSpacingY(); @@ -394,7 +394,7 @@ sliceThickness_ = axial.GetPixelSpacingX(); Vector origin = axial.GetGeometry().GetOrigin(); - origin += (static_cast<double>(volume.GetSliceCount() - 1) * + origin += (static_cast<double>(volume.GetSlicesCount() - 1) * axialThickness * axial.GetGeometry().GetNormal()); reference_ = CoordinateSystem3D(origin, @@ -406,7 +406,7 @@ VolumeImageGeometry(const OrthancVolumeImage& volume, VolumeProjection projection) { - if (volume.GetSliceCount() == 0) + if (volume.GetSlicesCount() == 0) { throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); } @@ -432,7 +432,7 @@ } } - size_t GetSliceCount() const + size_t GetSlicesCount() const { return depth_; } @@ -728,7 +728,7 @@ dynamic_cast<const OrthancVolumeImage&>(message.GetOrigin()); slices_.reset(new VolumeImageGeometry(image, projection_)); - SetSlice(slices_->GetSliceCount() / 2); + SetSlice(slices_->GetSlicesCount() / 2); widget_.FitContent(); } @@ -817,7 +817,7 @@ return slices_.get() != NULL; } - size_t GetSliceCount() const + size_t GetSlicesCount() const { if (slices_.get() == NULL) { @@ -825,7 +825,7 @@ } else { - return slices_->GetSliceCount(); + return slices_->GetSlicesCount(); } } @@ -840,9 +840,9 @@ slice = 0; } - if (slice >= static_cast<int>(slices_->GetSliceCount())) + if (slice >= static_cast<int>(slices_->GetSlicesCount())) { - slice = static_cast<unsigned int>(slices_->GetSliceCount()) - 1; + slice = static_cast<unsigned int>(slices_->GetSlicesCount()) - 1; } if (slice != static_cast<int>(slice_))
--- a/Samples/Sdl/Loader.cpp Tue May 14 09:48:14 2019 +0200 +++ b/Samples/Sdl/Loader.cpp Tue May 14 09:49:24 2019 +0200 @@ -26,6 +26,7 @@ #include "../../Framework/StoneInitialization.h" #include "../../Framework/Toolbox/GeometryToolbox.h" #include "../../Framework/Volumes/ImageBuffer3D.h" +#include "../../Framework/Toolbox/SlicesSorter.h" // From Orthanc framework #include <Core/Compression/GzipCompressor.h> @@ -899,8 +900,6 @@ if (object.get() != NULL) { - printf("===========================> REQUEST\n"); - const Item& item = dynamic_cast<Item&>(*object); try @@ -1140,7 +1139,8 @@ - class DicomInstanceParameters : public boost::noncopyable + class DicomInstanceParameters : + public Orthanc::IDynamicObject /* to be used as a payload of SlicesSorter */ { private: Orthanc::DicomImageInformation imageInformation_; @@ -1374,13 +1374,10 @@ tmp = GetFrameGeometry(frame); } - bool opposite; // Ignored - return (OrthancStone::GeometryToolbox::IsParallelOrOpposite( - opposite, tmp.GetNormal(), plane.GetNormal()) && - OrthancStone::LinearAlgebra::IsNear( - tmp.ProjectAlongNormal(tmp.GetOrigin()), - tmp.ProjectAlongNormal(plane.GetOrigin()), - thickness_ / 2.0)); + double distance; + + return (OrthancStone::CoordinateSystem3D::GetDistance(distance, tmp, plane) && + distance <= thickness_ / 2.0); } bool IsColor() const @@ -1496,8 +1493,19 @@ Orthanc::DicomMap dicom; dicom.FromDicomAsJson(value[instances[i]]); - DicomInstanceParameters instance(dicom); + std::auto_ptr<DicomInstanceParameters> instance(new DicomInstanceParameters(dicom)); + + OrthancStone::CoordinateSystem3D geometry = instance->GetGeometry(); + that_.slices_.AddSlice(geometry, instance.release()); } + + if (!that_.slices_.Sort()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, + "Cannot sort the 3D slices of a DICOM series"); + } + + printf("series sorted\n"); } }; @@ -1533,7 +1541,7 @@ bool active_; std::auto_ptr<OrthancStone::ImageBuffer3D> image_; - + OrthancStone::SlicesSorter slices_; public: AxialVolumeOrthancLoader(OrthancStone::IObservable& oracle) :