# HG changeset patch # User Sebastien Jodogne # Date 1582561177 -3600 # Node ID 12253ddefe5aff1fe5b8be092a61a1fe0bb92982 # Parent 5f64c866108ad6d58e4ff588fcaa987fbdd004d6 skeleton for new route: /instances/{id}/rendered diff -r 5f64c866108a -r 12253ddefe5a Core/Images/ImageProcessing.cpp --- a/Core/Images/ImageProcessing.cpp Mon Feb 24 16:26:59 2020 +0100 +++ b/Core/Images/ImageProcessing.cpp Mon Feb 24 17:19:37 2020 +0100 @@ -548,13 +548,13 @@ } } - void ImageProcessing::ApplyWindowing(ImageAccessor& target, - const ImageAccessor& source, - float windowCenter, - float windowWidth, - float rescaleSlope, - float rescaleIntercept, - bool invert) + void ImageProcessing::ApplyWindowing_Deprecated(ImageAccessor& target, + const ImageAccessor& source, + float windowCenter, + float windowWidth, + float rescaleSlope, + float rescaleIntercept, + bool invert) { if (target.GetWidth() != source.GetWidth() || target.GetHeight() != source.GetHeight()) diff -r 5f64c866108a -r 12253ddefe5a Core/Images/ImageProcessing.h --- a/Core/Images/ImageProcessing.h Mon Feb 24 16:26:59 2020 +0100 +++ b/Core/Images/ImageProcessing.h Mon Feb 24 17:19:37 2020 +0100 @@ -82,13 +82,13 @@ void Convert(ImageAccessor& target, const ImageAccessor& source); - void ApplyWindowing(ImageAccessor& target, - const ImageAccessor& source, - float windowCenter, - float windowWidth, - float rescaleSlope, - float rescaleIntercept, - bool invert); + void ApplyWindowing_Deprecated(ImageAccessor& target, + const ImageAccessor& source, + float windowCenter, + float windowWidth, + float rescaleSlope, + float rescaleIntercept, + bool invert); void Set(ImageAccessor& image, int64_t value); diff -r 5f64c866108a -r 12253ddefe5a NEWS --- a/NEWS Mon Feb 24 16:26:59 2020 +0100 +++ b/NEWS Mon Feb 24 17:19:37 2020 +0100 @@ -7,9 +7,11 @@ * API version has been upgraded to 5 * added "/peers/{id}/system" route to test the connectivity with a remote peer (and eventually retrieve its version number) -* /changes: Allow the "limit" argument to be greater than 100 -* /instances/{id}/preview: Now takes the windowing into account -* /tools/log-level: Possibility to access and change the log level without restarting Orthanc +* "/changes": Allow the "limit" argument to be greater than 100 +* "/instances/{id}/preview": Now takes the windowing into account +* "/tools/log-level": Possibility to access and change the log level without restarting Orthanc +* added "/instances/{id}/frames/{frame}/rendered" and "/instances/{id}/rendered" routes + to render frames, taking windowing and resizing into account Plugins ------- diff -r 5f64c866108a -r 12253ddefe5a OrthancServer/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Mon Feb 24 16:26:59 2020 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Mon Feb 24 17:19:37 2020 +0100 @@ -35,11 +35,11 @@ #include "OrthancRestApi.h" #include "../../Core/Compression/GzipCompressor.h" +#include "../../Core/DicomFormat/DicomImageInformation.h" #include "../../Core/DicomParsing/DicomWebJsonVisitor.h" #include "../../Core/DicomParsing/FromDcmtkBridge.h" #include "../../Core/DicomParsing/Internals/DicomImageDecoder.h" #include "../../Core/HttpServer/HttpContentNegociation.h" -#include "../../Core/Images/ImageProcessing.h" #include "../../Core/Logging.h" #include "../DefaultDicomImageDecoder.h" #include "../OrthancConfiguration.h" @@ -503,152 +503,222 @@ } - void LookupWindowingTags(const ParsedDicomFile& dicom, float& windowCenter, float& windowWidth, float& rescaleSlope, float& rescaleIntercept, bool& invert) + namespace { - DicomMap dicomTags; - dicom.ExtractDicomSummary(dicomTags); + class IDecodedFrameHandler : public boost::noncopyable + { + public: + virtual ~IDecodedFrameHandler() + { + } + + virtual void Handle(RestApiGetCall& call, + std::auto_ptr& decoded, + const DicomMap& dicom) = 0; + + virtual bool RequiresDicomTags() const = 0; + + static void Apply(RestApiGetCall& call, + IDecodedFrameHandler& handler) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string frameId = call.GetUriComponent("frame", "0"); + + unsigned int frame; + try + { + frame = boost::lexical_cast(frameId); + } + catch (boost::bad_lexical_cast&) + { + return; + } + + DicomMap dicom; + std::auto_ptr decoded; + + try + { + std::string publicId = call.GetUriComponent("id", ""); + +#if ORTHANC_ENABLE_PLUGINS == 1 + if (context.GetPlugins().HasCustomImageDecoder()) + { + // TODO create a cache of file + std::string dicomContent; + context.ReadDicom(dicomContent, publicId); + decoded.reset(context.GetPlugins().DecodeUnsafe(dicomContent.c_str(), dicomContent.size(), frame)); + + /** + * Note that we call "DecodeUnsafe()": We do not fallback to + * the builtin decoder if no installed decoder plugin is able + * to decode the image. This allows us to take advantage of + * the cache below. + **/ + + if (handler.RequiresDicomTags() && + decoded.get() != NULL) + { + // TODO Optimize this lookup for photometric interpretation: + // It should be implemented by the plugin to avoid parsing + // twice the DICOM file + ParsedDicomFile parsed(dicomContent); + parsed.ExtractDicomSummary(dicom); + } + } +#endif + + if (decoded.get() == NULL) + { + // Use Orthanc's built-in decoder, using the cache to speed-up + // things on multi-frame images + ServerContext::DicomCacheLocker locker(context, publicId); + decoded.reset(DicomImageDecoder::Decode(locker.GetDicom(), frame)); + + if (handler.RequiresDicomTags()) + { + locker.GetDicom().ExtractDicomSummary(dicom); + } + } + } + catch (OrthancException& e) + { + if (e.GetErrorCode() == ErrorCode_ParameterOutOfRange || + e.GetErrorCode() == ErrorCode_UnknownResource) + { + // The frame number is out of the range for this DICOM + // instance, the resource is not existent + } + else + { + std::string root = ""; + for (size_t i = 1; i < call.GetFullUri().size(); i++) + { + root += "../"; + } + + call.GetOutput().Redirect(root + "app/images/unsupported.png"); + } + return; + } + + handler.Handle(call, decoded, dicom); + } + }; - unsigned int bitsStored = boost::lexical_cast(dicomTags.GetStringValue(Orthanc::DICOM_TAG_BITS_STORED, "8", false)); - windowWidth = static_cast(2 << (bitsStored - 1)); - windowCenter = windowWidth / 2; - rescaleSlope = 1.0f; - rescaleIntercept = 0.0f; - invert = false; + class GetImageHandler : public IDecodedFrameHandler + { + private: + ImageExtractionMode mode_; + + public: + GetImageHandler(ImageExtractionMode mode) : + mode_(mode) + { + } + + virtual void Handle(RestApiGetCall& call, + std::auto_ptr& decoded, + const DicomMap& dicom) ORTHANC_OVERRIDE + { + bool invert = false; + + if (mode_ == ImageExtractionMode_Preview) + { + DicomImageInformation info(dicom); + invert = (info.GetPhotometricInterpretation() == PhotometricInterpretation_Monochrome1); + } - if (dicomTags.HasTag(Orthanc::DICOM_TAG_WINDOW_CENTER) && dicomTags.HasTag(Orthanc::DICOM_TAG_WINDOW_WIDTH)) - { - dicomTags.ParseFloat(windowCenter, Orthanc::DICOM_TAG_WINDOW_CENTER); - dicomTags.ParseFloat(windowWidth, Orthanc::DICOM_TAG_WINDOW_WIDTH); - } + ImageToEncode image(decoded, mode_, invert); + + HttpContentNegociation negociation; + EncodePng png(image); + negociation.Register(MIME_PNG, png); + + EncodeJpeg jpeg(image, call); + negociation.Register(MIME_JPEG, jpeg); + + EncodePam pam(image); + negociation.Register(MIME_PAM, pam); + + if (negociation.Apply(call.GetHttpHeaders())) + { + image.Answer(call.GetOutput()); + } + } + + virtual bool RequiresDicomTags() const ORTHANC_OVERRIDE + { + return mode_ == ImageExtractionMode_Preview; + } + }; + - if (dicomTags.HasTag(Orthanc::DICOM_TAG_RESCALE_SLOPE) && dicomTags.HasTag(Orthanc::DICOM_TAG_RESCALE_INTERCEPT)) + class RenderedFrameHandler : public IDecodedFrameHandler { - dicomTags.ParseFloat(rescaleSlope, Orthanc::DICOM_TAG_RESCALE_SLOPE); - dicomTags.ParseFloat(rescaleIntercept, Orthanc::DICOM_TAG_RESCALE_INTERCEPT); - } + private: + static void LookupWindowingTags(const DicomMap& dicom, + float& windowCenter, + float& windowWidth, + float& rescaleSlope, + float& rescaleIntercept, + bool& invert) + { + DicomImageInformation info(dicom); + + windowWidth = static_cast(1 << info.GetBitsStored()); + windowCenter = windowWidth / 2.0f; + rescaleSlope = 1.0f; + rescaleIntercept = 0.0f; + invert = false; + + if (dicom.HasTag(Orthanc::DICOM_TAG_WINDOW_CENTER) && + dicom.HasTag(Orthanc::DICOM_TAG_WINDOW_WIDTH)) + { + dicom.ParseFloat(windowCenter, Orthanc::DICOM_TAG_WINDOW_CENTER); + dicom.ParseFloat(windowWidth, Orthanc::DICOM_TAG_WINDOW_WIDTH); + } - PhotometricInterpretation photometric; - if (dicom.LookupPhotometricInterpretation(photometric)) - { - invert = (photometric == PhotometricInterpretation_Monochrome1); - } + if (dicom.HasTag(Orthanc::DICOM_TAG_RESCALE_SLOPE) && + dicom.HasTag(Orthanc::DICOM_TAG_RESCALE_INTERCEPT)) + { + dicom.ParseFloat(rescaleSlope, Orthanc::DICOM_TAG_RESCALE_SLOPE); + dicom.ParseFloat(rescaleIntercept, Orthanc::DICOM_TAG_RESCALE_INTERCEPT); + } + + invert = (info.GetPhotometricInterpretation() == PhotometricInterpretation_Monochrome1); + } + + public: + virtual void Handle(RestApiGetCall& call, + std::auto_ptr& decoded, + const DicomMap& dicom) ORTHANC_OVERRIDE + { + // TODO + } + + virtual bool RequiresDicomTags() const ORTHANC_OVERRIDE + { + return true; + } + }; } + template static void GetImage(RestApiGetCall& call) { - ServerContext& context = OrthancRestApi::GetContext(call); - - std::string frameId = call.GetUriComponent("frame", "0"); - - unsigned int frame; - try - { - frame = boost::lexical_cast(frameId); - } - catch (boost::bad_lexical_cast&) - { - return; - } - - bool invert = false; - float windowCenter = 128.0f; - float windowWidth = 256.0f; - float rescaleSlope = 1.0f; - float rescaleIntercept = 0.0f; - - std::auto_ptr decoded; - - try - { - std::string publicId = call.GetUriComponent("id", ""); + GetImageHandler handler(mode); + IDecodedFrameHandler::Apply(call, handler); + } -#if ORTHANC_ENABLE_PLUGINS == 1 - if (context.GetPlugins().HasCustomImageDecoder()) - { - // TODO create a cache of file - std::string dicomContent; - context.ReadDicom(dicomContent, publicId); - decoded.reset(context.GetPlugins().DecodeUnsafe(dicomContent.c_str(), dicomContent.size(), frame)); - - /** - * Note that we call "DecodeUnsafe()": We do not fallback to - * the builtin decoder if no installed decoder plugin is able - * to decode the image. This allows us to take advantage of - * the cache below. - **/ - - if (mode == ImageExtractionMode_Preview && - decoded.get() != NULL) - { - // TODO Optimize this lookup for photometric interpretation: - // It should be implemented by the plugin to avoid parsing - // twice the DICOM file - ParsedDicomFile parsed(dicomContent); - - LookupWindowingTags(dicomContent, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); - } - } -#endif - if (decoded.get() == NULL) - { - // Use Orthanc's built-in decoder, using the cache to speed-up - // things on multi-frame images - ServerContext::DicomCacheLocker locker(context, publicId); - decoded.reset(DicomImageDecoder::Decode(locker.GetDicom(), frame)); - LookupWindowingTags(locker.GetDicom(), windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); - - if (mode != ImageExtractionMode_Preview) - { - invert = false; - } - } - } - catch (OrthancException& e) - { - if (e.GetErrorCode() == ErrorCode_ParameterOutOfRange || e.GetErrorCode() == ErrorCode_UnknownResource) - { - // The frame number is out of the range for this DICOM - // instance, the resource is not existent - } - else - { - std::string root = ""; - for (size_t i = 1; i < call.GetFullUri().size(); i++) - { - root += "../"; - } - - call.GetOutput().Redirect(root + "app/images/unsupported.png"); - } - return; - } - - if (mode == ImageExtractionMode_Preview - && (decoded->GetFormat() == Orthanc::PixelFormat_Grayscale8 || decoded->GetFormat() == Orthanc::PixelFormat_Grayscale16)) - { - ImageProcessing::ApplyWindowing(*decoded, *decoded, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); - invert = false; // don't invert it later on when encoding it, it has been inverted in the ApplyWindowing function - } - - ImageToEncode image(decoded, mode, invert); - - HttpContentNegociation negociation; - EncodePng png(image); - negociation.Register(MIME_PNG, png); - - EncodeJpeg jpeg(image, call); - negociation.Register(MIME_JPEG, jpeg); - - EncodePam pam(image); - negociation.Register(MIME_PAM, pam); - - if (negociation.Apply(call.GetHttpHeaders())) - { - image.Answer(call.GetOutput()); - } + static void GetRenderedFrame(RestApiGetCall& call) + { + RenderedFrameHandler handler; + IDecodedFrameHandler::Apply(call, handler); } @@ -1802,6 +1872,7 @@ Register("/instances/{id}/frames", ListFrames); Register("/instances/{id}/frames/{frame}/preview", GetImage); + Register("/instances/{id}/frames/{frame}/rendered", GetRenderedFrame); Register("/instances/{id}/frames/{frame}/image-uint8", GetImage); Register("/instances/{id}/frames/{frame}/image-uint16", GetImage); Register("/instances/{id}/frames/{frame}/image-int16", GetImage); @@ -1810,6 +1881,7 @@ Register("/instances/{id}/frames/{frame}/raw.gz", GetRawFrame); Register("/instances/{id}/pdf", ExtractPdf); Register("/instances/{id}/preview", GetImage); + Register("/instances/{id}/rendered", GetRenderedFrame); Register("/instances/{id}/image-uint8", GetImage); Register("/instances/{id}/image-uint16", GetImage); Register("/instances/{id}/image-int16", GetImage); diff -r 5f64c866108a -r 12253ddefe5a UnitTestsSources/ImageProcessingTests.cpp --- a/UnitTestsSources/ImageProcessingTests.cpp Mon Feb 24 16:26:59 2020 +0100 +++ b/UnitTestsSources/ImageProcessingTests.cpp Mon Feb 24 17:19:37 2020 +0100 @@ -826,7 +826,7 @@ { Image target(PixelFormat_Grayscale8, 6, 1, false); - ImageProcessing::ApplyWindowing(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false); + ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false); ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 0)); ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 0)); @@ -838,7 +838,7 @@ { Image target(PixelFormat_Grayscale8, 6, 1, false); - ImageProcessing::ApplyWindowing(target, image, 5.0f, 10.0f, 1.0f, 0.0f, true); + ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, true); ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 255)); ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 255)); @@ -850,7 +850,7 @@ { Image target(PixelFormat_Grayscale8, 6, 1, false); - ImageProcessing::ApplyWindowing(target, image, 5000.0f, 10000.01f, 1000.0f, 0.0f, false); + ImageProcessing::ApplyWindowing_Deprecated(target, image, 5000.0f, 10000.01f, 1000.0f, 0.0f, false); ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 0)); ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 0)); @@ -862,7 +862,7 @@ { Image target(PixelFormat_Grayscale8, 6, 1, false); - ImageProcessing::ApplyWindowing(target, image, 5000.0f, 10000.01f, 1000.0f, 0.0f, true); + ImageProcessing::ApplyWindowing_Deprecated(target, image, 5000.0f, 10000.01f, 1000.0f, 0.0f, true); ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 255)); ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 255)); @@ -874,7 +874,7 @@ { Image target(PixelFormat_Grayscale8, 6, 1, false); - ImageProcessing::ApplyWindowing(target, image, 50.0f, 100.1f, 10.0f, 30.0f, false); + ImageProcessing::ApplyWindowing_Deprecated(target, image, 50.0f, 100.1f, 10.0f, 30.0f, false); ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 0)); // (-5 * 10) + 30 => pixel value = -20 => 0 ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 256*30/100)); // ((0 * 10) + 30 => pixel value = 30 => 30% @@ -900,7 +900,7 @@ { Image target(PixelFormat_Grayscale16, 6, 1, false); - ImageProcessing::ApplyWindowing(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false); + ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false); ASSERT_TRUE(TestGrayscale16Pixel(target, 0, 0, 0)); ASSERT_TRUE(TestGrayscale16Pixel(target, 1, 0, 0)); @@ -924,7 +924,7 @@ { Image target(PixelFormat_Grayscale16, 5, 1, false); - ImageProcessing::ApplyWindowing(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false); + ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false); ASSERT_TRUE(TestGrayscale16Pixel(target, 0, 0, 0)); ASSERT_TRUE(TestGrayscale16Pixel(target, 1, 0, 65536*2/10)); @@ -947,7 +947,7 @@ { Image target(PixelFormat_Grayscale16, 5, 1, false); - ImageProcessing::ApplyWindowing(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false); + ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false); ASSERT_TRUE(TestGrayscale16Pixel(target, 0, 0, 0)); ASSERT_TRUE(TestGrayscale16Pixel(target, 1, 0, 65536*2/10));