Mercurial > hg > orthanc-dicomweb
changeset 352:9db71a9d0d8b
Support of "window", "viewport" and "quality" parameters in "Retrieve Rendered Transaction"
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Fri, 23 Aug 2019 17:31:08 +0200 |
parents | 17910c948abe |
children | 351db3241ea6 ec6b362b90b2 |
files | NEWS Plugin/WadoRsRetrieveRendered.cpp Status.txt |
diffstat | 3 files changed, 416 insertions(+), 18 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Fri Aug 23 14:16:13 2019 +0200 +++ b/NEWS Fri Aug 23 17:31:08 2019 +0200 @@ -1,6 +1,7 @@ Pending changes in the mainline =============================== +* Support of "window", "viewport" and "quality" parameters in "Retrieve Rendered Transaction" * Added explicit "Accept" header to avoid uncompressing DICOM files by Google cloud https://groups.google.com/d/msg/orthanc-users/w1Ekrsc6-U8/T2a_DoQ5CwAJ
--- a/Plugin/WadoRsRetrieveRendered.cpp Fri Aug 23 14:16:13 2019 +0200 +++ b/Plugin/WadoRsRetrieveRendered.cpp Fri Aug 23 17:31:08 2019 +0200 @@ -36,13 +36,16 @@ namespace ImageProcessing { template <PixelFormat Format> - void ResizeInternal(ImageAccessor& target, - const ImageAccessor& source) - { + static void ResizeInternal(ImageAccessor& target, + const ImageAccessor& source) + { + assert(target.GetFormat() == source.GetFormat() && + target.GetFormat() == Format); + + const unsigned int sourceWidth = source.GetWidth(); const unsigned int sourceHeight = source.GetHeight(); - const unsigned int sourceWidth = source.GetWidth(); + const unsigned int targetWidth = target.GetWidth(); const unsigned int targetHeight = target.GetHeight(); - const unsigned int targetWidth = target.GetWidth(); if (targetWidth == 0 || targetHeight == 0) { @@ -120,7 +123,8 @@ } } - + + void Resize(ImageAccessor& target, const ImageAccessor& source) { @@ -150,17 +154,114 @@ throw OrthancException(ErrorCode_NotImplemented); } } + + + Orthanc::ImageAccessor* Halve(const ImageAccessor& source) + { + std::auto_ptr<Orthanc::Image> target(new Orthanc::Image(source.GetFormat(), source.GetWidth() / 2, + source.GetHeight() / 2, false)); + Resize(*target, source); + return target.release(); + } + + + template <PixelFormat Format> + static void FlipXInternal(ImageAccessor& image) + { + const unsigned int height = image.GetHeight(); + const unsigned int width = image.GetWidth(); + + for (unsigned int y = 0; y < height; y++) + { + for (unsigned int x1 = 0; x1 < width / 2; x1++) + { + unsigned int x2 = width - 1 - x1; + + typename ImageTraits<Format>::PixelType a, b; + ImageTraits<Format>::GetPixel(a, image, x1, y); + ImageTraits<Format>::GetPixel(b, image, x2, y); + ImageTraits<Format>::SetPixel(image, a, x2, y); + ImageTraits<Format>::SetPixel(image, b, x1, y); + } + } + } + + + void FlipX(ImageAccessor& image) + { + switch (image.GetFormat()) + { + case PixelFormat_Grayscale8: + FlipXInternal<PixelFormat_Grayscale8>(image); + break; + + case PixelFormat_RGB24: + FlipXInternal<PixelFormat_RGB24>(image); + break; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + template <PixelFormat Format> + static void FlipYInternal(ImageAccessor& image) + { + const unsigned int height = image.GetHeight(); + const unsigned int width = image.GetWidth(); + + for (unsigned int y1 = 0; y1 < height / 2; y1++) + { + unsigned int y2 = height - 1 - y1; + + for (unsigned int x = 0; x < width; x++) + { + typename ImageTraits<Format>::PixelType a, b; + ImageTraits<Format>::GetPixel(a, image, x, y1); + ImageTraits<Format>::GetPixel(b, image, x, y2); + ImageTraits<Format>::SetPixel(image, a, x, y2); + ImageTraits<Format>::SetPixel(image, b, x, y1); + } + } + } + + + void FlipY(ImageAccessor& image) + { + switch (image.GetFormat()) + { + case PixelFormat_Grayscale8: + FlipYInternal<PixelFormat_Grayscale8>(image); + break; + + case PixelFormat_RGB24: + FlipYInternal<PixelFormat_RGB24>(image); + break; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } } } namespace { + enum WindowingMode + { + WindowingMode_Linear, + WindowingMode_LinearExact, + WindowingMode_Sigmoid + }; + class RenderingParameters : public boost::noncopyable { private: bool hasViewport_; bool hasQuality_; + bool hasWindowing_; bool hasVW_; bool hasVH_; bool hasSW_; @@ -174,6 +275,9 @@ bool flipX_; bool flipY_; unsigned int quality_; + float windowCenter_; + float windowWidth_; + WindowingMode windowingMode_; static bool GetIntegerValue(int& target, std::vector<std::string>& tokens, @@ -219,6 +323,7 @@ RenderingParameters(const OrthancPluginHttpRequest* request) : hasViewport_(false), hasQuality_(false), + hasWindowing_(false), hasVW_(false), hasVH_(false), hasSW_(false), @@ -229,9 +334,15 @@ sy_(0), sw_(0), sh_(0), - quality_(90) // Default quality for JPEG previews (the same as in Orthanc core) + flipX_(false), + flipY_(false), + quality_(90), // Default quality for JPEG previews (the same as in Orthanc core) + windowCenter_(128), + windowWidth_(256), + windowingMode_(WindowingMode_Linear) { static const std::string VIEWPORT("\"viewport\" in WADO-RS Retrieve Rendered Transaction"); + static const std::string WINDOWING("\"windowing\" in WADO-RS Retrieve Rendered Transaction"); for (uint32_t i = 0; i < request->getCount; i++) { @@ -285,17 +396,17 @@ sy_ = 0; // Default is zero } - hasSW_ = GetIntegerValue(tmp, tokens, 0, true, true, VIEWPORT); + hasSW_ = GetIntegerValue(tmp, tokens, 4, true, true, VIEWPORT); if (hasSW_) { - vw_ = static_cast<unsigned int>(tmp < 0 ? -tmp : tmp); // Take absolute value + sw_ = static_cast<unsigned int>(tmp < 0 ? -tmp : tmp); // Take absolute value flipX_ = (tmp < 0); } - hasSH_ = GetIntegerValue(tmp, tokens, 1, true, true, VIEWPORT); + hasSH_ = GetIntegerValue(tmp, tokens, 5, true, true, VIEWPORT); if (hasSH_) { - vh_ = static_cast<unsigned int>(tmp < 0 ? -tmp : tmp); // Take absolute value + sh_ = static_cast<unsigned int>(tmp < 0 ? -tmp : tmp); // Take absolute value flipY_ = (tmp < 0); } } @@ -323,23 +434,80 @@ quality_ = static_cast<unsigned int>(q); } + else if (key == "windowing") + { + hasWindowing_ = true; + + std::vector<std::string> tokens; + Orthanc::Toolbox::TokenizeString(tokens, value, ','); + + if (tokens.size() != 3) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, + "The number arguments to " + WINDOWING + " must be 3"); + } + + try + { + windowCenter_ = boost::lexical_cast<float>(tokens[0]); + windowWidth_ = boost::lexical_cast<float>(tokens[1]); + } + catch (boost::bad_lexical_cast&) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, + "The first and second arguments to " + WINDOWING + " must be floats: " + value); + } + + if (tokens[2] == "linear") + { + windowingMode_ = WindowingMode_Linear; + } + else if (tokens[2] == "linear-exact") + { + windowingMode_ = WindowingMode_LinearExact; + } + else if (tokens[2] == "sigmoid") + { + windowingMode_ = WindowingMode_Sigmoid; + } + else + { + throw Orthanc::OrthancException( + Orthanc::ErrorCode_ParameterOutOfRange, + "The third argument to " + WINDOWING + " must be linear, linear-exact or sigmoid: " + tokens[2]); + } + } } } bool HasCustomization() const { - return (hasViewport_ || hasQuality_); + return (hasViewport_ || hasQuality_ || hasWindowing_); } unsigned int GetTargetWidth(unsigned int sourceWidth) const { - return (hasVW_ ? vw_ : sourceWidth); + if (hasVW_) + { + return vw_; + } + else + { + return sourceWidth; + } } unsigned int GetTargetHeight(unsigned int sourceHeight) const { - return (hasVH_ ? vh_ : sourceHeight); + if (hasVH_) + { + return vh_; + } + else + { + return sourceHeight; + } } bool IsFlipX() const @@ -388,6 +556,26 @@ { return quality_; } + + bool IsWindowing() const + { + return hasWindowing_; + } + + float GetWindowCenter() const + { + return windowCenter_; + } + + float GetWindowWidth() const + { + return windowWidth_; + } + + WindowingMode GetWindowingMode() const + { + return windowingMode_; + } }; } @@ -432,13 +620,218 @@ } +template <Orthanc::PixelFormat SourceFormat> +static void ApplyWindowing(Orthanc::ImageAccessor& target, + const Orthanc::ImageAccessor& source, + float c, + float w, + WindowingMode mode) +{ + assert(target.GetFormat() == Orthanc::PixelFormat_Grayscale8 && + source.GetFormat() == SourceFormat); + + if (source.GetWidth() != target.GetWidth() || + source.GetHeight() != target.GetHeight()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize); + } + + const unsigned int width = source.GetWidth(); + const unsigned int height = source.GetHeight(); + + const float ymin = 0; + const float ymax = 255; + + + /** + + LINEAR: + http://dicom.nema.org/MEDICAL/dicom/2019a/output/chtml/part03/sect_C.11.2.html#sect_C.11.2.1.2.1 + + Python + ------ + + import sympy as sym + x, c, w, ymin, ymax = sym.symbols('x c w ymin ymax') + + e = ((x - (c - 0.5)) / (w-1) + 0.5) * (ymax- ymin) + ymin + print(sym.simplify(sym.collect(sym.expand(e), [ x, ymin, ymax ]))) + + Result + ------ + + (x*(ymax - ymin) + ymax*(-c + 0.5*w) + ymin*(c + 0.5*w - 1.0))/(w - 1) + + **/ + + const float linearXMin = (c - 0.5f - (w - 1.0f) / 2.0f); + const float linearXMax = (c - 0.5f + (w - 1.0f) / 2.0f); + const float linearYScaling = (ymax - ymin) / (w - 1.0f); + const float linearYOffset = (ymax * (-c + 0.5f * w) + ymin * (c + 0.5f * w - 1.0f)) / (w - 1.0f); + + + /** + + LINEAR-EXACT: + http://dicom.nema.org/MEDICAL/dicom/2019a/output/chtml/part03/sect_C.11.2.html#sect_C.11.2.1.3.2 + + Python + ------ + + import sympy as sym + x, c, w, ymin, ymax = sym.symbols('x c w ymin ymax') + + e = (x - c) / w * (ymax- ymin) + ymin + print(sym.simplify(sym.collect(sym.expand(e), [ x, ymin, ymax ]))) + + Result + ------ + + (-c*ymax + x*(ymax - ymin) + ymin*(c + w))/w + + **/ + const float exactXMin = (c - w / 2.0f); + const float exactXMax = (c + w / 2.0f); + const float exactYScaling = (ymax - ymin) / w; + const float exactYOffset = (-c * ymax + ymin * (c + w)) / w; + + + for (unsigned int y = 0; y < height; y++) + { + for (unsigned int x = 0; x < width; x++) + { + float a = Orthanc::ImageTraits<SourceFormat>::GetFloatPixel(source, x, y); + float b; + + switch (mode) + { + case WindowingMode_Linear: + { + if (a <= linearXMin) + { + b = ymin; + } + else if (a > linearXMax) + { + b = ymax; + } + else + { + b = a * linearYScaling + linearYOffset; + } + + break; + } + + case WindowingMode_LinearExact: + { + if (a <= exactXMin) + { + b = ymin; + } + else if (a > exactXMax) + { + b = ymax; + } + else + { + b = a * exactYScaling + exactYOffset; + } + + break; + } + + case WindowingMode_Sigmoid: + { + // http://dicom.nema.org/MEDICAL/dicom/2019a/output/chtml/part03/sect_C.11.2.html#sect_C.11.2.1.3.1 + b = ymax / (1.0f + expf(-4.0f * (a - c) / w)); + break; + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + Orthanc::ImageTraits<Orthanc::PixelFormat_Grayscale8>::SetFloatPixel(target, b, x, y); + } + } +} + + static void ApplyRendering(Orthanc::ImageAccessor& target, const Orthanc::ImageAccessor& source, const RenderingParameters& parameters) { - Orthanc::Image tmp(target.GetFormat(), source.GetWidth(), source.GetHeight(), false); - Orthanc::ImageProcessing::Convert(tmp, source); - Orthanc::ImageProcessing::Resize(target, tmp); + Orthanc::ImageProcessing::Set(target, 0); + + Orthanc::ImageAccessor region; + parameters.GetSourceRegion(region, source); + + Orthanc::Image scaled(target.GetFormat(), region.GetWidth(), region.GetHeight(), false); + + if (scaled.GetWidth() == 0 || + scaled.GetHeight() == 0) + { + return; + } + + switch (target.GetFormat()) + { + case Orthanc::PixelFormat_RGB24: + Orthanc::ImageProcessing::Convert(scaled, region); + break; + + case Orthanc::PixelFormat_Grayscale8: + { + switch (source.GetFormat()) + { + case Orthanc::PixelFormat_Grayscale16: + ApplyWindowing<Orthanc::PixelFormat_Grayscale16>(scaled, region, parameters.GetWindowCenter(), + parameters.GetWindowWidth(), + parameters.GetWindowingMode()); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + break; + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + if (parameters.IsFlipX()) + { + Orthanc::ImageProcessing::FlipX(scaled); + } + + if (parameters.IsFlipY()) + { + Orthanc::ImageProcessing::FlipY(scaled); + } + + // Preserve the aspect ratio + float cw = static_cast<float>(scaled.GetWidth()); + float ch = static_cast<float>(scaled.GetHeight()); + float r = std::min( + static_cast<float>(target.GetWidth()) / cw, + static_cast<float>(target.GetHeight()) / ch); + + unsigned int sw = std::min(static_cast<unsigned int>(boost::math::iround(cw * r)), target.GetWidth()); + unsigned int sh = std::min(static_cast<unsigned int>(boost::math::iround(ch * r)), target.GetHeight()); + Orthanc::Image resized(target.GetFormat(), sw, sh, false); + + Orthanc::ImageProcessing::Resize(resized, scaled); + + assert(target.GetWidth() >= resized.GetWidth() && + target.GetHeight() >= resized.GetHeight()); + unsigned int offsetX = (target.GetWidth() - resized.GetWidth()) / 2; + unsigned int offsetY = (target.GetHeight() - resized.GetHeight()) / 2; + + target.GetRegion(region, offsetX, offsetY, resized.GetWidth(), resized.GetHeight()); + Orthanc::ImageProcessing::Copy(region, resized); }
--- a/Status.txt Fri Aug 23 14:16:13 2019 +0200 +++ b/Status.txt Fri Aug 23 17:31:08 2019 +0200 @@ -93,12 +93,16 @@ * Single-frame and multi-frame retrieval * JPEG and PNG output +* "quality" parameter +* "viewport" parameter +* "window" parameter Not supported ------------- * GIF output -* None of the "Retrieve Rendered Query Parameters" (table 6.5.8-2) +* The following "Retrieve Rendered Query Parameters" (table 6.5.8-2): + annotation, charset, iccprofile