changeset 3683:12253ddefe5a

skeleton for new route: /instances/{id}/rendered
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 24 Feb 2020 17:19:37 +0100
parents 5f64c866108a
children 3971ec6b1f72
files Core/Images/ImageProcessing.cpp Core/Images/ImageProcessing.h NEWS OrthancServer/OrthancRestApi/OrthancRestResources.cpp UnitTestsSources/ImageProcessingTests.cpp
diffstat 5 files changed, 232 insertions(+), 158 deletions(-) [+]
line wrap: on
line diff
--- 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())
--- 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);
--- 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
 -------
--- 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<ImageAccessor>& 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<unsigned int>(frameId);
+        }
+        catch (boost::bad_lexical_cast&)
+        {
+          return;
+        }
+
+        DicomMap dicom;
+        std::auto_ptr<ImageAccessor> 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<unsigned int>(dicomTags.GetStringValue(Orthanc::DICOM_TAG_BITS_STORED, "8", false));
-    windowWidth = static_cast<float>(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<ImageAccessor>& 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<float>(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<ImageAccessor>& decoded,
+                          const DicomMap& dicom) ORTHANC_OVERRIDE
+      {
+        // TODO
+      }
+
+      virtual bool RequiresDicomTags() const ORTHANC_OVERRIDE
+      {
+        return true;
+      }
+    };
   }
 
+
   template <enum ImageExtractionMode mode>
   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<unsigned int>(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<ImageAccessor> 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<ImageExtractionMode_Preview>);
+    Register("/instances/{id}/frames/{frame}/rendered", GetRenderedFrame);
     Register("/instances/{id}/frames/{frame}/image-uint8", GetImage<ImageExtractionMode_UInt8>);
     Register("/instances/{id}/frames/{frame}/image-uint16", GetImage<ImageExtractionMode_UInt16>);
     Register("/instances/{id}/frames/{frame}/image-int16", GetImage<ImageExtractionMode_Int16>);
@@ -1810,6 +1881,7 @@
     Register("/instances/{id}/frames/{frame}/raw.gz", GetRawFrame<true>);
     Register("/instances/{id}/pdf", ExtractPdf);
     Register("/instances/{id}/preview", GetImage<ImageExtractionMode_Preview>);
+    Register("/instances/{id}/rendered", GetRenderedFrame);
     Register("/instances/{id}/image-uint8", GetImage<ImageExtractionMode_UInt8>);
     Register("/instances/{id}/image-uint16", GetImage<ImageExtractionMode_UInt16>);
     Register("/instances/{id}/image-int16", GetImage<ImageExtractionMode_Int16>);
--- 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));