Mercurial > hg > orthanc-stone
changeset 1279:7ec8fea061b9 broker
integration mainline->broker
line wrap: on
line diff
--- a/.hgtags Fri Jan 31 17:34:57 2020 +0100 +++ b/.hgtags Tue Feb 04 15:20:08 2020 +0100 @@ -40,3 +40,6 @@ 33b0a762e98ade4e1f00e26bf865b417269443fb toa2019110401 76705b430c78d4ca0a718c6164ac4b73ce007fde toa2019110801 ce3052f28f2e6e7b29cfad4efae4e3881e70b056 toa2019122001 +5a2d5380148d4068af55ca3e895f5fa18c778bc1 toa2020012701 +ca2058bd74eff429033df3de7a6785ae08a92922 toa2020012702 +5e45322c77249573661bb4f45f035d000db446fc toa2020012703
--- a/Applications/Qt/QCairoWidget.cpp Fri Jan 31 17:34:57 2020 +0100 +++ b/Applications/Qt/QCairoWidget.cpp Tue Feb 04 15:20:08 2020 +0100 @@ -181,6 +181,18 @@ CASE_QT_KEY_TO_ORTHANC(Qt::Key_Down, KeyboardKeys_Down); CASE_QT_KEY_TO_ORTHANC(Qt::Key_Left, KeyboardKeys_Left); CASE_QT_KEY_TO_ORTHANC(Qt::Key_Right, KeyboardKeys_Right); + CASE_QT_KEY_TO_ORTHANC(Qt::Key_F1, KeyboardKeys_F1); + CASE_QT_KEY_TO_ORTHANC(Qt::Key_F2, KeyboardKeys_F2); + CASE_QT_KEY_TO_ORTHANC(Qt::Key_F3, KeyboardKeys_F3); + CASE_QT_KEY_TO_ORTHANC(Qt::Key_F4, KeyboardKeys_F4); + CASE_QT_KEY_TO_ORTHANC(Qt::Key_F5, KeyboardKeys_F5); + CASE_QT_KEY_TO_ORTHANC(Qt::Key_F6, KeyboardKeys_F6); + CASE_QT_KEY_TO_ORTHANC(Qt::Key_F7, KeyboardKeys_F7); + CASE_QT_KEY_TO_ORTHANC(Qt::Key_F8, KeyboardKeys_F8); + CASE_QT_KEY_TO_ORTHANC(Qt::Key_F9, KeyboardKeys_F9); + CASE_QT_KEY_TO_ORTHANC(Qt::Key_F10, KeyboardKeys_F10); + CASE_QT_KEY_TO_ORTHANC(Qt::Key_F11, KeyboardKeys_F11); + CASE_QT_KEY_TO_ORTHANC(Qt::Key_F12, KeyboardKeys_F12); default: break; }
--- a/Applications/Samples/SingleFrameEditorApplication.h Fri Jan 31 17:34:57 2020 +0100 +++ b/Applications/Samples/SingleFrameEditorApplication.h Tue Feb 04 15:20:08 2020 +0100 @@ -315,7 +315,7 @@ << snapshot.toStyledString(); boost::shared_ptr<RadiographyScene> scene(new RadiographyScene); - RadiographySceneReader reader(*scene, context_->GetOrthancApiClient()); + RadiographySceneReader reader(*scene, *context_->GetOrthancApiClient()); reader.Read(snapshot); widget.SetScene(scene);
--- a/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.cpp Fri Jan 31 17:34:57 2020 +0100 +++ b/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.cpp Tue Feb 04 15:20:08 2020 +0100 @@ -263,7 +263,8 @@ } template <typename T> - void OrthancMultiframeVolumeLoader::CopyPixelData(const std::string& pixelData) + void OrthancMultiframeVolumeLoader::CopyPixelDataAndComputeDistribution( + const std::string& pixelData, std::map<T,uint64_t>& distribution) { OrthancStone::ImageBuffer3D& target = volume_->GetPixelData(); @@ -283,43 +284,209 @@ return; } - const uint8_t* source = reinterpret_cast<const uint8_t*>(pixelData.c_str()); - - for (unsigned int z = 0; z < depth; z++) + // first pass to initialize map { - OrthancStone::ImageBuffer3D::SliceWriter writer(target, OrthancStone::VolumeProjection_Axial, z); + const uint8_t* source = reinterpret_cast<const uint8_t*>(pixelData.c_str()); - assert (writer.GetAccessor().GetWidth() == width && - writer.GetAccessor().GetHeight() == height); + for (unsigned int z = 0; z < depth; z++) + { + for (unsigned int y = 0; y < height; y++) + { + for (unsigned int x = 0; x < width; x++) + { + T value; + CopyPixel(value, source); + distribution[value] = 0; + source += bpp; + } + } + } + } + + { + const uint8_t* source = reinterpret_cast<const uint8_t*>(pixelData.c_str()); - for (unsigned int y = 0; y < height; y++) + for (unsigned int z = 0; z < depth; z++) { - assert(sizeof(T) == Orthanc::GetBytesPerPixel(target.GetFormat())); + OrthancStone::ImageBuffer3D::SliceWriter writer(target, OrthancStone::VolumeProjection_Axial, z); - T* target = reinterpret_cast<T*>(writer.GetAccessor().GetRow(y)); + assert(writer.GetAccessor().GetWidth() == width && + writer.GetAccessor().GetHeight() == height); + + for (unsigned int y = 0; y < height; y++) + { + assert(sizeof(T) == Orthanc::GetBytesPerPixel(target.GetFormat())); - for (unsigned int x = 0; x < width; x++) - { - CopyPixel(*target, source); - target ++; - source += bpp; + T* target = reinterpret_cast<T*>(writer.GetAccessor().GetRow(y)); + + for (unsigned int x = 0; x < width; x++) + { + CopyPixel(*target, source); + + distribution[*target] += 1; + + target++; + source += bpp; + } } } } } + template <typename T> + void OrthancMultiframeVolumeLoader::ComputeMinMaxWithOutlierRejection( + const std::map<T, uint64_t>& distribution) + { + if (distribution.size() == 0) + { + LOG(ERROR) << "ComputeMinMaxWithOutlierRejection -- Volume image empty."; + } + else + { + OrthancStone::ImageBuffer3D& target = volume_->GetPixelData(); + + const uint64_t bpp = target.GetBytesPerPixel(); + const uint64_t width = target.GetWidth(); + const uint64_t height = target.GetHeight(); + const uint64_t depth = target.GetDepth(); + const uint64_t voxelCount = width * height * depth; + + // now that we have distribution[pixelValue] == numberOfPixelsWithValue + // compute number of values and check (assertion) that it is equal to + // width * height * depth + { + typename std::map<T, uint64_t>::const_iterator it = distribution.begin(); + uint64_t totalCount = 0; + distributionRawMin_ = static_cast<float>(it->first); + + while (it != distribution.end()) + { + T pixelValue = it->first; + uint64_t count = it->second; + totalCount += count; + it++; + if (it == distribution.end()) + distributionRawMax_ = static_cast<float>(pixelValue); + } + LOG(INFO) << "Volume image. First distribution value = " + << static_cast<float>(distributionRawMin_) + << " | Last distribution value = " + << static_cast<float>(distributionRawMax_); + + if (totalCount != voxelCount) + { + LOG(ERROR) << "Internal error in dose distribution computation. TC (" + << totalCount << ") != VoxC (" << voxelCount; + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + } + + // compute the number of voxels to reject at each end of the distribution + uint64_t endRejectionCount = static_cast<uint64_t>( + outliersHalfRejectionRate_ * voxelCount); + + if (endRejectionCount > voxelCount) + { + LOG(ERROR) << "Internal error in dose distribution computation." + << " endRejectionCount = " << endRejectionCount + << " | voxelCount = " << voxelCount; + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + // this will contain the actual distribution minimum after outlier + // rejection + T resultMin = 0; + + // then start from start and remove pixel values up to + // endRejectionCount voxels rejected + { + typename std::map<T, uint64_t>::const_iterator it = distribution.begin(); + + uint64_t currentCount = 0; + + while (it != distribution.end()) + { + T pixelValue = it->first; + uint64_t count = it->second; + + // if this pixelValue crosses the rejection threshold, let's set it + // and exit the loop + if ((currentCount <= endRejectionCount) && + (currentCount + count > endRejectionCount)) + { + resultMin = pixelValue; + break; + } + else + { + currentCount += count; + } + // and continue walking along the distribution + it++; + } + } + + // this will contain the actual distribution maximum after outlier + // rejection + T resultMax = 0; + // now start from END and remove pixel values up to + // endRejectionCount voxels rejected + { + typename std::map<T, uint64_t>::const_reverse_iterator it = distribution.rbegin(); + + uint64_t currentCount = 0; + + while (it != distribution.rend()) + { + T pixelValue = it->first; + uint64_t count = it->second; + + if ((currentCount <= endRejectionCount) && + (currentCount + count > endRejectionCount)) + { + resultMax = pixelValue; + break; + } + else + { + currentCount += count; + } + // and continue walking along the distribution + it++; + } + } + if (resultMin > resultMax) + { + LOG(ERROR) << "Internal error in dose distribution computation! " << + "resultMin (" << resultMin << ") > resultMax (" << resultMax << ")"; + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + computedDistributionMin_ = static_cast<float>(resultMin); + computedDistributionMax_ = static_cast<float>(resultMax); + } + } + + template <typename T> + void OrthancMultiframeVolumeLoader::CopyPixelDataAndComputeMinMax( + const std::string& pixelData) + { + std::map<T, uint64_t> distribution; + CopyPixelDataAndComputeDistribution(pixelData, distribution); + ComputeMinMaxWithOutlierRejection(distribution); + } + void OrthancMultiframeVolumeLoader::SetUncompressedPixelData(const std::string& pixelData) { switch (volume_->GetPixelData().GetFormat()) { case Orthanc::PixelFormat_Grayscale32: - CopyPixelData<uint32_t>(pixelData); + CopyPixelDataAndComputeMinMax<uint32_t>(pixelData); break; case Orthanc::PixelFormat_Grayscale16: - CopyPixelData<uint16_t>(pixelData); + CopyPixelDataAndComputeMinMax<uint16_t>(pixelData); break; case Orthanc::PixelFormat_SignedGrayscale16: - CopyPixelData<int16_t>(pixelData); + CopyPixelDataAndComputeMinMax<int16_t>(pixelData); break; default: throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); @@ -341,12 +508,19 @@ return volume_->GetGeometry(); } - OrthancMultiframeVolumeLoader::OrthancMultiframeVolumeLoader(boost::shared_ptr<OrthancStone::DicomVolumeImage> volume, - OrthancStone::IOracle& oracle, - OrthancStone::IObservable& oracleObservable) : + OrthancMultiframeVolumeLoader::OrthancMultiframeVolumeLoader( + boost::shared_ptr<OrthancStone::DicomVolumeImage> volume, + OrthancStone::IOracle& oracle, + OrthancStone::IObservable& oracleObservable, + float outliersHalfRejectionRate) : LoaderStateMachine(oracle, oracleObservable), volume_(volume), - pixelDataLoaded_(false) + pixelDataLoaded_(false), + outliersHalfRejectionRate_(outliersHalfRejectionRate), + distributionRawMin_(0), + distributionRawMax_(0), + computedDistributionMin_(0), + computedDistributionMax_(0) { if (volume.get() == NULL) { @@ -359,6 +533,29 @@ LOG(TRACE) << "OrthancMultiframeVolumeLoader::~OrthancMultiframeVolumeLoader()"; } + + void OrthancMultiframeVolumeLoader::GetDistributionMinMax + (float& minValue, float& maxValue) const + { + if (distributionRawMin_ == 0 && distributionRawMax_ == 0) + { + LOG(WARNING) << "GetDistributionMinMaxWithOutliersRejection called before computation!"; + } + minValue = distributionRawMin_; + maxValue = distributionRawMax_; + } + + void OrthancMultiframeVolumeLoader::GetDistributionMinMaxWithOutliersRejection + (float& minValue, float& maxValue) const + { + if (computedDistributionMin_ == 0 && computedDistributionMax_ == 0) + { + LOG(WARNING) << "GetDistributionMinMaxWithOutliersRejection called before computation!"; + } + minValue = computedDistributionMin_; + maxValue = computedDistributionMax_; + } + void OrthancMultiframeVolumeLoader::LoadInstance(const std::string& instanceId) { Start();
--- a/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.h Fri Jan 31 17:34:57 2020 +0100 +++ b/Framework/Deprecated/Loaders/OrthancMultiframeVolumeLoader.h Tue Feb 04 15:20:08 2020 +0100 @@ -42,6 +42,11 @@ std::string instanceId_; std::string transferSyntaxUid_; bool pixelDataLoaded_; + float outliersHalfRejectionRate_; + float distributionRawMin_; + float distributionRawMax_; + float computedDistributionMin_; + float computedDistributionMax_; const std::string& GetInstanceId() const; @@ -51,8 +56,35 @@ void SetGeometry(const Orthanc::DicomMap& dicom); + + /** + This method will : + + - copy the pixel values from the response to the volume image + - compute the maximum and minimum value while discarding the + outliersHalfRejectionRate_ fraction of the outliers from both the start + and the end of the distribution. + + In English, this means that, if the volume dataset contains a few extreme + values very different from the rest (outliers) that we want to get rid of, + this method allows to do so. + + If you supply 0.005, for instance, it means 1% of the extreme values will + be rejected (0.5% on each side of the distribution) + */ template <typename T> - void CopyPixelData(const std::string& pixelData); + void CopyPixelDataAndComputeMinMax(const std::string& pixelData); + + /** Service method for CopyPixelDataAndComputeMinMax*/ + template <typename T> + void CopyPixelDataAndComputeDistribution( + const std::string& pixelData, + std::map<T, uint64_t>& distribution); + + /** Service method for CopyPixelDataAndComputeMinMax*/ + template <typename T> + void ComputeMinMaxWithOutlierRejection( + const std::map<T, uint64_t>& distribution); void SetUncompressedPixelData(const std::string& pixelData); @@ -62,7 +94,8 @@ public: OrthancMultiframeVolumeLoader(boost::shared_ptr<OrthancStone::DicomVolumeImage> volume, OrthancStone::IOracle& oracle, - OrthancStone::IObservable& oracleObservable); + OrthancStone::IObservable& oracleObservable, + float outliersHalfRejectionRate = 0.0005); virtual ~OrthancMultiframeVolumeLoader(); @@ -71,6 +104,12 @@ return pixelDataLoaded_; } + void GetDistributionMinMax + (float& minValue, float& maxValue) const; + + void GetDistributionMinMaxWithOutliersRejection + (float& minValue, float& maxValue) const; + void LoadInstance(const std::string& instanceId); }; }
--- a/Framework/Messages/ICallable.h Fri Jan 31 17:34:57 2020 +0100 +++ b/Framework/Messages/ICallable.h Tue Feb 04 15:20:08 2020 +0100 @@ -30,6 +30,7 @@ #include <boost/weak_ptr.hpp> #include <string> +#include <stdint.h> namespace OrthancStone {
--- a/Framework/Messages/IObserver.h Fri Jan 31 17:34:57 2020 +0100 +++ b/Framework/Messages/IObserver.h Tue Feb 04 15:20:08 2020 +0100 @@ -23,6 +23,8 @@ #include <boost/noncopyable.hpp> +#include <stdint.h> + namespace OrthancStone { class IObserver : public boost::noncopyable
--- a/Framework/Radiography/RadiographyDicomLayer.cpp Fri Jan 31 17:34:57 2020 +0100 +++ b/Framework/Radiography/RadiographyDicomLayer.cpp Tue Feb 04 15:20:08 2020 +0100 @@ -113,7 +113,7 @@ BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this)); } - void RadiographyDicomLayer::SetSourceImage(Orthanc::ImageAccessor* image, double newPixelSpacingX, double newPixelSpacingY) // Takes ownership + void RadiographyDicomLayer::SetSourceImage(Orthanc::ImageAccessor* image, double newPixelSpacingX, double newPixelSpacingY, bool emitLayerEditedEvent) // Takes ownership { std::auto_ptr<Orthanc::ImageAccessor> raii(image); @@ -122,14 +122,17 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); } - SetSize(image->GetWidth(), image->GetHeight()); + SetSize(image->GetWidth(), image->GetHeight(), false); source_ = raii; ApplyConverter(); SetPixelSpacing(newPixelSpacingX, newPixelSpacingY, false); - BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this)); + if (emitLayerEditedEvent) + { + BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this)); + } }
--- a/Framework/Radiography/RadiographyDicomLayer.h Fri Jan 31 17:34:57 2020 +0100 +++ b/Framework/Radiography/RadiographyDicomLayer.h Tue Feb 04 15:20:08 2020 +0100 @@ -80,7 +80,7 @@ void SetSourceImage(Orthanc::ImageAccessor* image); // Takes ownership - void SetSourceImage(Orthanc::ImageAccessor* image, double newPixelSpacingX, double newPixelSpacingY); // Takes ownership + void SetSourceImage(Orthanc::ImageAccessor* image, double newPixelSpacingX, double newPixelSpacingY, bool emitLayerEditedEvent = true); // Takes ownership const Orthanc::ImageAccessor* GetSourceImage() const {return source_.get();} // currently need this access to serialize scene in plain old data to send to a WASM worker
--- a/Framework/Radiography/RadiographyLayer.cpp Fri Jan 31 17:34:57 2020 +0100 +++ b/Framework/Radiography/RadiographyLayer.cpp Tue Feb 04 15:20:08 2020 +0100 @@ -59,15 +59,15 @@ void RadiographyLayer::UpdateTransform() { + // important to update transform_ before getting the center to use the right scaling !!! transform_ = AffineTransform2D::CreateScaling(geometry_.GetScalingX(), geometry_.GetScalingY()); double centerX, centerY; GetCenter(centerX, centerY); transform_ = AffineTransform2D::Combine( - AffineTransform2D::CreateOffset(geometry_.GetPanX() + centerX, geometry_.GetPanY() + centerY), - AffineTransform2D::CreateRotation(geometry_.GetAngle()), - AffineTransform2D::CreateOffset(-centerX, -centerY), + AffineTransform2D::CreateOffset(geometry_.GetPanX(), geometry_.GetPanY()), + AffineTransform2D::CreateRotation(geometry_.GetAngle(), centerX, centerY), transform_); transformInverse_ = AffineTransform2D::Invert(transform_); @@ -222,14 +222,19 @@ } void RadiographyLayer::SetSize(unsigned int width, - unsigned int height) + unsigned int height, + bool emitLayerEditedEvent) { hasSize_ = true; width_ = width; height_ = height; UpdateTransform(); - BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this)); + + if (emitLayerEditedEvent) + { + BroadcastMessage(RadiographyLayer::LayerEditedMessage(*this)); + } }
--- a/Framework/Radiography/RadiographyLayer.h Fri Jan 31 17:34:57 2020 +0100 +++ b/Framework/Radiography/RadiographyLayer.h Tue Feb 04 15:20:08 2020 +0100 @@ -217,16 +217,6 @@ const RadiographyScene& scene_; protected: - virtual const AffineTransform2D& GetTransform() const - { - return transform_; - } - - virtual const AffineTransform2D& GetTransformInverse() const - { - return transformInverse_; - } - void SetPreferredPhotomotricDisplayMode(RadiographyPhotometricDisplayMode prefferedPhotometricDisplayMode); private: @@ -254,6 +244,16 @@ { } + virtual const AffineTransform2D& GetTransform() const + { + return transform_; + } + + virtual const AffineTransform2D& GetTransformInverse() const + { + return transformInverse_; + } + size_t GetIndex() const { return index_; @@ -298,7 +298,8 @@ } void SetSize(unsigned int width, - unsigned int height); + unsigned int height, + bool emitLayerEditedEvent = true); bool HasSize() const { @@ -358,8 +359,6 @@ virtual bool GetRange(float& minValue, float& maxValue) const = 0; - friend class RadiographyMaskLayer; // because it needs to GetTransform on the dicomLayer it relates to - virtual size_t GetApproximateMemoryUsage() const // this is used to limit the number of scenes loaded in RAM when resources are limited (we actually only count the size used by the images, not the C structs) { return 0;
--- a/Framework/Radiography/RadiographyMaskLayer.cpp Fri Jan 31 17:34:57 2020 +0100 +++ b/Framework/Radiography/RadiographyMaskLayer.cpp Tue Feb 04 15:20:08 2020 +0100 @@ -82,7 +82,7 @@ float windowWidth, bool applyWindowing) const { - if (dicomLayer_.GetWidth() == 0) // nothing to do if the DICOM layer is not displayed (or not loaded) + if (dicomLayer_.GetWidth() == 0 || dicomLayer_.GetSourceImage() == NULL) // nothing to do if the DICOM layer is not displayed (or not loaded) return; if (invalidated_)
--- a/Framework/Radiography/RadiographyScene.cpp Fri Jan 31 17:34:57 2020 +0100 +++ b/Framework/Radiography/RadiographyScene.cpp Tue Feb 04 15:20:08 2020 +0100 @@ -125,6 +125,17 @@ } } + void RadiographyScene::_RegisterLayer(RadiographyLayer* layer) + { + std::auto_ptr<RadiographyLayer> raii(layer); + + // LOG(INFO) << "Registering layer: " << countLayers_; + + size_t index = nextLayerIndex_++; + raii->SetIndex(index); + layers_[index] = raii.release(); + } + RadiographyLayer& RadiographyScene::RegisterLayer(RadiographyLayer* layer) { if (layer == NULL) @@ -132,11 +143,7 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); } - std::auto_ptr<RadiographyLayer> raii(layer); - - size_t index = nextLayerIndex_++; - raii->SetIndex(index); - layers_[index] = raii.release(); + _RegisterLayer(layer); BroadcastMessage(GeometryChangedMessage(*this, *layer)); BroadcastMessage(ContentChangedMessage(*this, *layer)); @@ -221,6 +228,8 @@ LOG(INFO) << "Removing layer, there are now : " << layers_.size() << " layers"; + _OnLayerRemoved(); + BroadcastMessage(RadiographyScene::LayerRemovedMessage(*this, layerIndex)); } } @@ -556,11 +565,24 @@ // Render layers in the background-to-foreground order for (size_t index = 0; index < nextLayerIndex_; index++) { - Layers::const_iterator it = layers_.find(index); - if (it != layers_.end()) + try { - assert(it->second != NULL); - it->second->Render(buffer, viewTransform, interpolation, windowingCenter_, windowingWidth_, applyWindowing); + Layers::const_iterator it = layers_.find(index); + if (it != layers_.end()) + { + assert(it->second != NULL); + it->second->Render(buffer, viewTransform, interpolation, windowingCenter_, windowingWidth_, applyWindowing); + } + } + catch (Orthanc::OrthancException& ex) + { + LOG(ERROR) << "RadiographyScene::Render: " << index << ", OrthancException: " << ex.GetDetails(); + throw ex; // rethrow because we want it to crash to see there's a problem ! + } + catch (...) + { + LOG(ERROR) << "RadiographyScene::Render: " << index << ", unkown exception: "; + throw; // rethrow because we want it to crash to see there's a problem ! } } } @@ -637,6 +659,28 @@ } } + void RadiographyScene::ExtractLayerFromRenderedScene(Orthanc::ImageAccessor& layer, + const Orthanc::ImageAccessor& renderedScene, + size_t layerIndex, + ImageInterpolation interpolation) + { + Extent2D sceneExtent = GetSceneExtent(); + + double pixelSpacingX = sceneExtent.GetWidth() / renderedScene.GetWidth(); + double pixelSpacingY = sceneExtent.GetHeight() / renderedScene.GetHeight(); + + AffineTransform2D view = AffineTransform2D::Combine( + AffineTransform2D::CreateScaling(1.0 / pixelSpacingX, 1.0 / pixelSpacingY), + AffineTransform2D::CreateOffset(-sceneExtent.GetX1(), -sceneExtent.GetY1())); + + AffineTransform2D layerToSceneTransform = AffineTransform2D::Combine( + view, + GetLayer(layerIndex).GetTransform()); + + AffineTransform2D sceneToLayerTransform = AffineTransform2D::Invert(layerToSceneTransform); + sceneToLayerTransform.Apply(layer, renderedScene, interpolation, false); + } + Orthanc::Image* RadiographyScene::ExportToImage(double pixelSpacingX, double pixelSpacingY, ImageInterpolation interpolation, @@ -669,7 +713,14 @@ AffineTransform2D::CreateOffset(-extent.GetX1(), -extent.GetY1())); // wipe background before rendering - Orthanc::ImageProcessing::Set(layers, 0); + if (GetPreferredPhotomotricDisplayMode() == RadiographyPhotometricDisplayMode_Monochrome1) + { + Orthanc::ImageProcessing::Set(layers, 65535.0f); + } + else + { + Orthanc::ImageProcessing::Set(layers, 0); + } Render(layers, view, interpolation, applyWindowing);
--- a/Framework/Radiography/RadiographyScene.h Fri Jan 31 17:34:57 2020 +0100 +++ b/Framework/Radiography/RadiographyScene.h Tue Feb 04 15:20:08 2020 +0100 @@ -37,6 +37,7 @@ public ObserverBase<RadiographyScene>, public IObservable { + friend class RadiographySceneGeometryReader; public: class GeometryChangedMessage : public OriginMessage<RadiographyScene> { @@ -168,8 +169,17 @@ float windowingWidth_; Layers layers_; + public: + RadiographyLayer& RegisterLayer(RadiographyLayer* layer); + protected: - RadiographyLayer& RegisterLayer(RadiographyLayer* layer); + virtual void _RegisterLayer(RadiographyLayer* layer); + virtual void _OnLayerRemoved() {} + + void SetLayerIndex(RadiographyLayer* layer, size_t index) + { + layer->SetIndex(index); + } virtual void OnTagsReceived(const Deprecated::OrthancApiClient::BinaryResponseReadyMessage& message); @@ -341,5 +351,9 @@ int64_t maxValue /* for inversion */, bool applyWindowing); + void ExtractLayerFromRenderedScene(Orthanc::ImageAccessor& layer, + const Orthanc::ImageAccessor& renderedScene, + size_t layerIndex, + ImageInterpolation interpolation); }; }
--- a/Framework/Radiography/RadiographySceneReader.cpp Fri Jan 31 17:34:57 2020 +0100 +++ b/Framework/Radiography/RadiographySceneReader.cpp Tue Feb 04 15:20:08 2020 +0100 @@ -50,7 +50,17 @@ RadiographyDicomLayer* RadiographySceneReader::LoadDicom(const std::string& instanceId, unsigned int frame, RadiographyLayer::Geometry* geometry) { - return dynamic_cast<RadiographyDicomLayer*>(&(scene_.LoadDicomFrame(*orthancApiClient_, instanceId, frame, false, geometry))); + return dynamic_cast<RadiographyDicomLayer*>(&(scene_.LoadDicomFrame(orthancApiClient_, instanceId, frame, false, geometry))); + } + + RadiographyDicomLayer* RadiographySceneGeometryReader::LoadDicom(const std::string& instanceId, unsigned int frame, RadiographyLayer::Geometry* geometry) + { + std::auto_ptr<RadiographyPlaceholderLayer> layer(new RadiographyPlaceholderLayer(scene_)); + layer->SetGeometry(*geometry); + layer->SetSize(dicomImageWidth_, dicomImageHeight_); + scene_.RegisterLayer(layer.get()); + + return layer.release(); } void RadiographySceneBuilder::Read(const Json::Value& input) @@ -130,82 +140,8 @@ } } - void RadiographySceneReader::Read(const Json::Value& input) - { - unsigned int version = input["version"].asUInt(); - - if (version != 1) - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - - if (input.isMember("hasWindowing") && input["hasWindowing"].asBool()) - { - scene_.SetWindowing(input["windowCenter"].asFloat(), input["windowWidth"].asFloat()); - } - - RadiographyDicomLayer* dicomLayer = NULL; - for(size_t layerIndex = 0; layerIndex < input["layers"].size(); layerIndex++) - { - const Json::Value& jsonLayer = input["layers"][(int)layerIndex]; - RadiographyLayer::Geometry geometry; - - if (jsonLayer["type"].asString() == "dicom") - { - ReadLayerGeometry(geometry, jsonLayer); - dicomLayer = dynamic_cast<RadiographyDicomLayer*>(&(scene_.LoadDicomFrame(*orthancApiClient_, jsonLayer["instanceId"].asString(), jsonLayer["frame"].asUInt(), false, &geometry))); - } - else if (jsonLayer["type"].asString() == "mask") - { - if (dicomLayer == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); // we always assumed the dicom layer was read before the mask - } - ReadLayerGeometry(geometry, jsonLayer); - float foreground = jsonLayer["foreground"].asFloat(); - std::vector<Orthanc::ImageProcessing::ImagePoint> corners; - for (size_t i = 0; i < jsonLayer["corners"].size(); i++) - { - Orthanc::ImageProcessing::ImagePoint corner(jsonLayer["corners"][(int)i]["x"].asInt(), - jsonLayer["corners"][(int)i]["y"].asInt()); - corners.push_back(corner); - } - scene_.LoadMask(corners, *dicomLayer, foreground, &geometry); - } - else if (jsonLayer["type"].asString() == "text") - { - ReadLayerGeometry(geometry, jsonLayer); - scene_.LoadText(jsonLayer["text"].asString(), jsonLayer["font"].asString(), jsonLayer["fontSize"].asUInt(), static_cast<uint8_t>(jsonLayer["foreground"].asUInt()), &geometry, false); - } - else if (jsonLayer["type"].asString() == "alpha") - { - ReadLayerGeometry(geometry, jsonLayer); - - const std::string& pngContentBase64 = jsonLayer["content"].asString(); - std::string pngContent; - std::string mimeType; - Orthanc::Toolbox::DecodeDataUriScheme(mimeType, pngContent, pngContentBase64); - - std::auto_ptr<Orthanc::ImageAccessor> image; - if (mimeType == "image/png") - { - image.reset(new Orthanc::PngReader()); - dynamic_cast<Orthanc::PngReader*>(image.get())->ReadFromMemory(pngContent); - } - else - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - - RadiographyAlphaLayer& layer = dynamic_cast<RadiographyAlphaLayer&>(scene_.LoadAlphaBitmap(image.release(), &geometry)); - - if (!jsonLayer["isUsingWindowing"].asBool()) - { - layer.SetForegroundValue((float)(jsonLayer["foreground"].asDouble())); - } - } - else - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - } void RadiographySceneBuilder::ReadDicomLayerGeometry(RadiographyLayer::Geometry& geometry, const Json::Value& input) {
--- a/Framework/Radiography/RadiographySceneReader.h Fri Jan 31 17:34:57 2020 +0100 +++ b/Framework/Radiography/RadiographySceneReader.h Tue Feb 04 15:20:08 2020 +0100 @@ -33,6 +33,18 @@ namespace OrthancStone { + // a layer containing only the geometry of a DICOM layer (bit hacky !) + class RadiographyPlaceholderLayer : public RadiographyDicomLayer + { + public: + RadiographyPlaceholderLayer(const RadiographyScene& scene) : + RadiographyDicomLayer(scene) + { + } + + }; + + // HACK: I had to introduce this builder class in order to be able to recreate a RadiographyScene // from a serialized scene that is passed to web-workers. // It needs some architecturing... @@ -68,18 +80,32 @@ class RadiographySceneReader : public RadiographySceneBuilder { - private: - boost::shared_ptr<Deprecated::OrthancApiClient> orthancApiClient_; + Deprecated::OrthancApiClient& orthancApiClient_; public: - RadiographySceneReader(RadiographyScene& scene, - boost::shared_ptr<Deprecated::OrthancApiClient> orthancApiClient) : + RadiographySceneReader(RadiographyScene& scene, Deprecated::OrthancApiClient& orthancApiClient) : RadiographySceneBuilder(scene), orthancApiClient_(orthancApiClient) { } - void Read(const Json::Value& input); + protected: + virtual RadiographyDicomLayer* LoadDicom(const std::string& instanceId, unsigned int frame, RadiographyLayer::Geometry* geometry); + }; + + // reads the whole scene but the DICOM image such that we have the full geometry + class RadiographySceneGeometryReader : public RadiographySceneBuilder + { + unsigned int dicomImageWidth_; + unsigned int dicomImageHeight_; + + public: + RadiographySceneGeometryReader(RadiographyScene& scene, unsigned int dicomImageWidth, unsigned int dicomImageHeight) : + RadiographySceneBuilder(scene), + dicomImageWidth_(dicomImageWidth), + dicomImageHeight_(dicomImageHeight) + { + } protected: virtual RadiographyDicomLayer* LoadDicom(const std::string& instanceId, unsigned int frame, RadiographyLayer::Geometry* geometry);
--- a/Framework/Radiography/RadiographyWidget.cpp Fri Jan 31 17:34:57 2020 +0100 +++ b/Framework/Radiography/RadiographyWidget.cpp Tue Feb 04 15:20:08 2020 +0100 @@ -76,6 +76,11 @@ { floatBuffer_.reset(new Orthanc::Image( Orthanc::PixelFormat_Float32, width, height, false)); + + if (floatBuffer_.get() == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotEnoughMemory, "RadiographyWidget::RenderInternal: unable to allocate float buffer"); + } } if (cairoBuffer_.get() == NULL || @@ -83,6 +88,11 @@ cairoBuffer_->GetHeight() != height) { cairoBuffer_.reset(new CairoSurface(width, height, false /* no alpha */)); + + if (cairoBuffer_.get() == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotEnoughMemory, "RadiographyWidget::RenderInternal: unable to allocate cairo buffer"); + } } RenderBackground(*floatBuffer_, 0.0, 65535.0); @@ -188,6 +198,8 @@ void RadiographyWidget::Unselect() { hasSelection_ = false; + + NotifyContentChanged(); BroadcastMessage(SelectionChangedMessage(*this)); }
--- a/Framework/StoneEnumerations.h Fri Jan 31 17:34:57 2020 +0100 +++ b/Framework/StoneEnumerations.h Tue Feb 04 15:20:08 2020 +0100 @@ -86,7 +86,20 @@ KeyboardKeys_Up = 38, KeyboardKeys_Right = 39, KeyboardKeys_Down = 40, - KeyboardKeys_Delete = 46 + KeyboardKeys_Delete = 46, + + KeyboardKeys_F1 = 112, + KeyboardKeys_F2 = 113, + KeyboardKeys_F3 = 114, + KeyboardKeys_F4 = 115, + KeyboardKeys_F5 = 116, + KeyboardKeys_F6 = 117, + KeyboardKeys_F7 = 118, + KeyboardKeys_F8 = 119, + KeyboardKeys_F9 = 120, + KeyboardKeys_F10 = 121, + KeyboardKeys_F11 = 122, + KeyboardKeys_F12 = 123, }; enum SopClassUid
--- a/Framework/Toolbox/AffineTransform2D.cpp Fri Jan 31 17:34:57 2020 +0100 +++ b/Framework/Toolbox/AffineTransform2D.cpp Tue Feb 04 15:20:08 2020 +0100 @@ -246,6 +246,16 @@ return t; } + AffineTransform2D AffineTransform2D::CreateRotation(double angle, // CW rotation + double cx, // rotation center + double cy) // rotation center + { + return Combine( + CreateOffset(cx, cy), + CreateRotation(angle), + CreateOffset(-cx, -cy) + ); + } AffineTransform2D AffineTransform2D::CreateOpenGLClipspace(unsigned int canvasWidth, unsigned int canvasHeight)
--- a/Framework/Toolbox/AffineTransform2D.h Fri Jan 31 17:34:57 2020 +0100 +++ b/Framework/Toolbox/AffineTransform2D.h Tue Feb 04 15:20:08 2020 +0100 @@ -90,8 +90,12 @@ static AffineTransform2D CreateScaling(double sx, double sy); - - static AffineTransform2D CreateRotation(double angle); + + static AffineTransform2D CreateRotation(double angle); // CW rotation in radians + + static AffineTransform2D CreateRotation(double angle, // CW rotation in radians + double cx, // rotation center + double cy); // rotation center static AffineTransform2D CreateOpenGLClipspace(unsigned int canvasWidth, unsigned int canvasHeight);