changeset 3690:a9ce35d67c3c

implementation of "/instances/.../rendered" for grayscale images
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 25 Feb 2020 13:57:43 +0100
parents e85bfba2d307
children 4922bdd046dd
files Core/DicomFormat/DicomMap.cpp Core/DicomFormat/DicomMap.h Core/DicomFormat/DicomValue.cpp Core/DicomFormat/DicomValue.h Core/Images/ImageProcessing.cpp Core/Images/ImageProcessing.h OrthancServer/OrthancRestApi/OrthancRestResources.cpp
diffstat 7 files changed, 197 insertions(+), 55 deletions(-) [+]
line wrap: on
line diff
--- a/Core/DicomFormat/DicomMap.cpp	Tue Feb 25 11:07:56 2020 +0100
+++ b/Core/DicomFormat/DicomMap.cpp	Tue Feb 25 13:57:43 2020 +0100
@@ -984,6 +984,21 @@
     }
   }
 
+  bool DicomMap::ParseFirstFloat(float& result,
+                                 const DicomTag& tag) const
+  {
+    const DicomValue* value = TestAndGetValue(tag);
+
+    if (value == NULL)
+    {
+      return false;
+    }
+    else
+    {
+      return value->ParseFirstFloat(result);
+    }
+  }
+
   bool DicomMap::ParseDouble(double& result,
                              const DicomTag& tag) const
   {
--- a/Core/DicomFormat/DicomMap.h	Tue Feb 25 11:07:56 2020 +0100
+++ b/Core/DicomFormat/DicomMap.h	Tue Feb 25 13:57:43 2020 +0100
@@ -205,6 +205,9 @@
     bool ParseFloat(float& result,
                     const DicomTag& tag) const;
 
+    bool ParseFirstFloat(float& result,
+                         const DicomTag& tag) const;
+
     bool ParseDouble(double& result,
                      const DicomTag& tag) const;
 
--- a/Core/DicomFormat/DicomValue.cpp	Tue Feb 25 11:07:56 2020 +0100
+++ b/Core/DicomFormat/DicomValue.cpp	Tue Feb 25 13:57:43 2020 +0100
@@ -231,6 +231,11 @@
     return ParseValue<double, true>(result, *this);
   }
 
+  bool DicomValue::ParseFirstFloat(float& result) const
+  {
+    return ParseFirstValue<float, true>(result, *this);
+  }
+
   bool DicomValue::ParseFirstUnsignedInteger(unsigned int& result) const
   {
     return ParseFirstValue<unsigned int, true>(result, *this);
--- a/Core/DicomFormat/DicomValue.h	Tue Feb 25 11:07:56 2020 +0100
+++ b/Core/DicomFormat/DicomValue.h	Tue Feb 25 13:57:43 2020 +0100
@@ -112,6 +112,8 @@
 
     bool ParseDouble(double& result) const;
 
+    bool ParseFirstFloat(float& result) const;
+
     bool ParseFirstUnsignedInteger(unsigned int& result) const;
 
     void Serialize(Json::Value& target) const;
--- a/Core/Images/ImageProcessing.cpp	Tue Feb 25 11:07:56 2020 +0100
+++ b/Core/Images/ImageProcessing.cpp	Tue Feb 25 13:57:43 2020 +0100
@@ -1430,6 +1430,46 @@
   }
 
 
+  void ImageProcessing::ShiftScale(ImageAccessor& target,
+                                   const ImageAccessor& source,
+                                   float offset,
+                                   float scaling,
+                                   bool useRound)
+  {
+    // Rewrite "(x + offset) * scaling" as "a * x + b"
+
+    const float a = scaling;
+    const float b = offset * scaling;
+    
+    switch (target.GetFormat())
+    {
+      case PixelFormat_Grayscale8:
+
+        switch (source.GetFormat())
+        {
+          case PixelFormat_Float32:
+            if (useRound)
+            {
+              ShiftScaleInternal<uint8_t, float, true, false>(
+                target, source, a, b, std::numeric_limits<uint8_t>::min());
+            }
+            else
+            {
+              ShiftScaleInternal<uint8_t, float, false, false>(
+                target, source, a, b, std::numeric_limits<uint8_t>::min());
+            }
+            return;
+
+          default:
+            throw OrthancException(ErrorCode_NotImplemented);
+        }
+        
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
   void ImageProcessing::Invert(ImageAccessor& image, int64_t maxValue)
   {
     const unsigned int width = image.GetWidth();
--- a/Core/Images/ImageProcessing.h	Tue Feb 25 11:07:56 2020 +0100
+++ b/Core/Images/ImageProcessing.h	Tue Feb 25 13:57:43 2020 +0100
@@ -133,6 +133,12 @@
                     float scaling,
                     bool useRound);
 
+    void ShiftScale(ImageAccessor& target,
+                    const ImageAccessor& source,
+                    float offset,
+                    float scaling,
+                    bool useRound);
+
     void Invert(ImageAccessor& image);
 
     void Invert(ImageAccessor& image, int64_t maxValue);
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Tue Feb 25 11:07:56 2020 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Tue Feb 25 13:57:43 2020 +0100
@@ -672,23 +672,20 @@
 
     class RenderedFrameHandler : public IDecodedFrameHandler
     {
-    public:
-      virtual void Handle(RestApiGetCall& call,
-                          std::auto_ptr<ImageAccessor>& decoded,
-                          const DicomMap& dicom) ORTHANC_OVERRIDE
+    private:
+      static void GetDicomParameters(bool& invert,
+                                     float& rescaleSlope,
+                                     float& rescaleIntercept,
+                                     float& windowWidth,
+                                     float& windowCenter,
+                                     const DicomMap& dicom)
       {
-        static const char* ARG_WINDOW_CENTER = "window-center";
-        static const char* ARG_WINDOW_WIDTH = "window-width";
-        static const char* ARG_MAX_WIDTH = "max-width";
-        static const char* ARG_MAX_HEIGHT = "max-height";
-        static const char* ARG_SMOOTH = "smooth";
-        
         DicomImageInformation info(dicom);
 
-        const bool invert = (info.GetPhotometricInterpretation() == PhotometricInterpretation_Monochrome1);
-        
-        float rescaleSlope = 1.0f;
-        float rescaleIntercept = 0.0f;
+        invert = (info.GetPhotometricInterpretation() == PhotometricInterpretation_Monochrome1);
+
+        rescaleSlope = 1.0f;
+        rescaleIntercept = 0.0f;
 
         if (dicom.HasTag(Orthanc::DICOM_TAG_RESCALE_SLOPE) &&
             dicom.HasTag(Orthanc::DICOM_TAG_RESCALE_INTERCEPT))
@@ -697,15 +694,29 @@
           dicom.ParseFloat(rescaleIntercept, Orthanc::DICOM_TAG_RESCALE_INTERCEPT);
         }
 
-        float windowWidth = static_cast<float>(1 << info.GetBitsStored());
-        float windowCenter = windowWidth / 2.0f;
+        windowWidth = static_cast<float>(1 << info.GetBitsStored());
+        windowCenter = windowWidth / 2.0f;
 
         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);
+          dicom.ParseFirstFloat(windowCenter, Orthanc::DICOM_TAG_WINDOW_CENTER);
+          dicom.ParseFirstFloat(windowWidth, Orthanc::DICOM_TAG_WINDOW_WIDTH);
         }
+      }
+
+      static void GetUserArguments(float& windowWidth /* inout */,
+                                   float& windowCenter /* inout */,
+                                   unsigned int& argWidth,
+                                   unsigned int& argHeight,
+                                   bool& smooth,
+                                   RestApiGetCall& call)
+      {
+        static const char* ARG_WINDOW_CENTER = "window-center";
+        static const char* ARG_WINDOW_WIDTH = "window-width";
+        static const char* ARG_WIDTH = "width";
+        static const char* ARG_HEIGHT = "height";
+        static const char* ARG_SMOOTH = "smooth";
 
         if (call.HasArgument(ARG_WINDOW_WIDTH))
         {
@@ -733,54 +744,54 @@
           }
         }
 
-        unsigned int maxWidth = 0;
-        unsigned int maxHeight = 0;
+        argWidth = 0;
+        argHeight = 0;
 
-        if (call.HasArgument(ARG_MAX_WIDTH))
+        if (call.HasArgument(ARG_WIDTH))
         {
           try
           {
-            int tmp = boost::lexical_cast<int>(call.GetArgument(ARG_MAX_WIDTH, ""));
+            int tmp = boost::lexical_cast<int>(call.GetArgument(ARG_WIDTH, ""));
             if (tmp < 0)
             {
               throw OrthancException(ErrorCode_ParameterOutOfRange,
-                                     "Argument cannot be negative: " + std::string(ARG_MAX_WIDTH));
+                                     "Argument cannot be negative: " + std::string(ARG_WIDTH));
             }
             else
             {
-              maxWidth = static_cast<unsigned int>(tmp);
+              argWidth = static_cast<unsigned int>(tmp);
             }
           }
           catch (boost::bad_lexical_cast&)
           {
             throw OrthancException(ErrorCode_ParameterOutOfRange,
-                                   "Bad value for argument: " + std::string(ARG_MAX_WIDTH));
+                                   "Bad value for argument: " + std::string(ARG_WIDTH));
           }
         }
 
-        if (call.HasArgument(ARG_MAX_HEIGHT))
+        if (call.HasArgument(ARG_HEIGHT))
         {
           try
           {
-            int tmp = boost::lexical_cast<int>(call.GetArgument(ARG_MAX_HEIGHT, ""));
+            int tmp = boost::lexical_cast<int>(call.GetArgument(ARG_HEIGHT, ""));
             if (tmp < 0)
             {
               throw OrthancException(ErrorCode_ParameterOutOfRange,
-                                     "Argument cannot be negative: " + std::string(ARG_MAX_HEIGHT));
+                                     "Argument cannot be negative: " + std::string(ARG_HEIGHT));
             }
             else
             {
-              maxHeight = static_cast<unsigned int>(tmp);
+              argHeight = static_cast<unsigned int>(tmp);
             }
           }
           catch (boost::bad_lexical_cast&)
           {
             throw OrthancException(ErrorCode_ParameterOutOfRange,
-                                   "Bad value for argument: " + std::string(ARG_MAX_HEIGHT));
+                                   "Bad value for argument: " + std::string(ARG_HEIGHT));
           }
         }
 
-        bool smooth = true;
+        smooth = false;
 
         if (call.HasArgument(ARG_SMOOTH))
         {
@@ -801,58 +812,118 @@
                                    "Argument must be Boolean: " + std::string(ARG_SMOOTH));
           }
         }        
-
+      }
+                                
+      
+    public:
+      virtual void Handle(RestApiGetCall& call,
+                          std::auto_ptr<ImageAccessor>& decoded,
+                          const DicomMap& dicom) ORTHANC_OVERRIDE
+      {
+        bool invert;
+        float rescaleSlope, rescaleIntercept, windowWidth, windowCenter;
+        GetDicomParameters(invert, rescaleSlope, rescaleIntercept, windowWidth, windowCenter, dicom);
 
-        unsigned int width = decoded->GetWidth();
-        unsigned int height = decoded->GetHeight();
+        unsigned int argWidth, argHeight;
+        bool smooth;
+        GetUserArguments(windowWidth, windowCenter, argWidth, argHeight, smooth, call);
+
+        unsigned int targetWidth = decoded->GetWidth();
+        unsigned int targetHeight = decoded->GetHeight();
 
         if (decoded->GetWidth() != 0 &&
             decoded->GetHeight() != 0)
         {
           float ratio = 1;
 
-          if (maxWidth != 0)
+          if (argWidth != 0 &&
+              argHeight != 0)
           {
-            ratio = static_cast<float>(maxWidth) / static_cast<float>(decoded->GetWidth());
+            float ratioX = static_cast<float>(argWidth) / static_cast<float>(decoded->GetWidth());
+            float ratioY = static_cast<float>(argHeight) / static_cast<float>(decoded->GetHeight());
+            ratio = std::min(ratioX, ratioY);
           }
-
-          if (maxHeight != 0)
+          else if (argWidth != 0)
           {
-            float ratioY = static_cast<float>(maxHeight) / static_cast<float>(decoded->GetHeight());
-            if (ratioY < ratio)
-            {
-              ratio = ratioY;
-            }
+            ratio = static_cast<float>(argWidth) / static_cast<float>(decoded->GetWidth());
           }
-
-          width = boost::math::iround(ratio * static_cast<float>(decoded->GetWidth()));
-          height = boost::math::iround(ratio * static_cast<float>(decoded->GetHeight()));
+          else if (argHeight != 0)
+          {
+            ratio = static_cast<float>(argHeight) / static_cast<float>(decoded->GetHeight());
+          }
+          
+          targetWidth = boost::math::iround(ratio * static_cast<float>(decoded->GetWidth()));
+          targetHeight = boost::math::iround(ratio * static_cast<float>(decoded->GetHeight()));
         }
         
         if (decoded->GetFormat() == PixelFormat_RGB24)
         {
-          if (width == decoded->GetWidth() &&
-              height == decoded->GetHeight())
+          if (targetWidth == decoded->GetWidth() &&
+              targetHeight == decoded->GetHeight())
           {
             DefaultHandler(call, decoded, ImageExtractionMode_Preview, false);
           }
           else
           {
-            std::auto_ptr<ImageAccessor> rescaled(new Image(decoded->GetFormat(), width, height, false));
+            std::auto_ptr<ImageAccessor> resized(
+              new Image(decoded->GetFormat(), targetWidth, targetHeight, false));
+            
             if (smooth &&
-                (width < decoded->GetWidth() ||
-                 height < decoded->GetHeight()))
+                (targetWidth < decoded->GetWidth() ||
+                 targetHeight < decoded->GetHeight()))
             {
               ImageProcessing::SmoothGaussian5x5(*decoded);
             }
-            ImageProcessing::Resize(*rescaled, *decoded);
-            DefaultHandler(call, rescaled, ImageExtractionMode_Preview, false);
+            
+            ImageProcessing::Resize(*resized, *decoded);
+            DefaultHandler(call, resized, ImageExtractionMode_Preview, false);
           }
         }
         else
         {
-          // TODO : (1) convert to float32, (2) apply windowing, (3) possibly rescale
-          throw OrthancException(ErrorCode_NotImplemented);
+          // Grayscale image: (1) convert to Float32, (2) apply
+          // windowing to get a Grayscale8, (3) possibly resize
+
+          Image converted(PixelFormat_Float32, decoded->GetWidth(), decoded->GetHeight(), false);
+          ImageProcessing::Convert(converted, *decoded);
+
+          // Avoid divisions by zero
+          if (windowWidth <= 1.0f)
+          {
+            windowWidth = 1;
+          }
+
+          if (std::abs(rescaleSlope) <= 0.1f)
+          {
+            rescaleSlope = 0.1f;
+          }
+
+          const float scaling = 255.0f * rescaleSlope / windowWidth;
+          const float offset = (rescaleIntercept - windowCenter + windowWidth / 2.0f) / rescaleSlope;
+
+          std::auto_ptr<ImageAccessor> rescaled(new Image(PixelFormat_Grayscale8, decoded->GetWidth(), decoded->GetHeight(), false));
+          ImageProcessing::ShiftScale(*rescaled, converted, offset, scaling, false);
+
+          if (targetWidth == decoded->GetWidth() &&
+              targetHeight == decoded->GetHeight())
+          {
+            DefaultHandler(call, rescaled, ImageExtractionMode_UInt8, invert);
+          }
+          else
+          {
+            std::auto_ptr<ImageAccessor> resized(
+              new Image(PixelFormat_Grayscale8, targetWidth, targetHeight, false));
+            
+            if (smooth &&
+                (targetWidth < decoded->GetWidth() ||
+                 targetHeight < decoded->GetHeight()))
+            {
+              ImageProcessing::SmoothGaussian5x5(*rescaled);
+            }
+            
+            ImageProcessing::Resize(*resized, *rescaled);
+            DefaultHandler(call, resized, ImageExtractionMode_UInt8, invert);
+          }
         }
       }