diff --git a/.hg_archival.txt b/.hg_archival.txt index 5ca0181..cb35c1e 100644 --- a/.hg_archival.txt +++ b/.hg_archival.txt @@ -1,6 +1,6 @@ repo: d5f45924411123cfd02d035fd50b8e37536eadef -node: 01bae1ba951d3c66e55aba6129bcba0ef825d081 -branch: OrthancDicomWeb-1.6 +node: f5b64f680bfbea760169e72602498b946c58c234 +branch: OrthancDicomWeb-1.7 latesttag: null -latesttagdistance: 462 -changessincelatesttag: 491 +latesttagdistance: 469 +changessincelatesttag: 498 diff --git a/CMakeLists.txt b/CMakeLists.txt index 5cbb810..6bf4450 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,13 +21,13 @@ project(OrthancDicomWeb) -set(ORTHANC_DICOM_WEB_VERSION "1.6") +set(ORTHANC_DICOM_WEB_VERSION "1.7") if (ORTHANC_DICOM_WEB_VERSION STREQUAL "mainline") set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "mainline") set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg") else() - set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "1.9.3") + set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "1.9.7") set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web") endif() @@ -57,11 +57,20 @@ # Download and setup the Orthanc framework include(${CMAKE_SOURCE_DIR}/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake) -include_directories(${ORTHANC_FRAMEWORK_ROOT}) - if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "system") + if (ORTHANC_FRAMEWORK_USE_SHARED) + include(FindBoost) + find_package(Boost COMPONENTS regex thread) + + if (NOT Boost_FOUND) + message(FATAL_ERROR "Unable to locate Boost on this system") + endif() + + link_libraries(${Boost_LIBRARIES} jsoncpp) + endif() + link_libraries(${ORTHANC_FRAMEWORK_LIBRARIES}) - + set(USE_SYSTEM_GOOGLE_TEST ON CACHE BOOL "Use the system version of Google Test") set(USE_GOOGLE_TEST_DEBIAN_PACKAGE OFF CACHE BOOL "Use the sources of Google Test shipped with libgtest-dev (Debian only)") mark_as_advanced(USE_GOOGLE_TEST_DEBIAN_PACKAGE) @@ -77,6 +86,7 @@ set(USE_BOOST_ICONV ON) include(${ORTHANC_FRAMEWORK_ROOT}/../Resources/CMake/OrthancFrameworkConfiguration.cmake) + include_directories(${ORTHANC_FRAMEWORK_ROOT}) endif() diff --git a/NEWS b/NEWS index 5420472..d07c317 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,11 @@ Pending changes in the mainline =============================== + + +Version 1.7 (2021-08-31) +======================== + +* Detection of windowing and rescale in ".../rendered" for Philips multiframe images Version 1.6 (2021-05-07) diff --git a/Plugin/WadoRsRetrieveRendered.cpp b/Plugin/WadoRsRetrieveRendered.cpp index 3e06a27..d13b735 100644 --- a/Plugin/WadoRsRetrieveRendered.cpp +++ b/Plugin/WadoRsRetrieveRendered.cpp @@ -28,8 +28,66 @@ #include #include +#if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 9, 7) +# include +#else +# include +#endif + #include #include + + +static bool ParseFloat(float& target, + const std::string& source) +{ +#if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 9, 7) + return Orthanc::SerializationToolbox::ParseFloat(target, source); + +#else + // Emulation for older versions of the Orthanc framework + std::string s = Orthanc::Toolbox::StripSpaces(source); + + if (s.empty()) + { + return false; + } + else + { + try + { + target = boost::lexical_cast(s); + return true; + } + catch (boost::bad_lexical_cast&) + { + return false; + } + } +#endif +} + + +static bool ParseFirstFloat(float& target, + const std::string& source) +{ +#if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 9, 7) + return Orthanc::SerializationToolbox::ParseFirstFloat(target, source); + +#else + // Emulation for older versions of the Orthanc framework + std::vector tokens; + Orthanc::Toolbox::TokenizeString(tokens, source, '\\'); + if (tokens.empty()) + { + return false; + } + else + { + return ParseFloat(target, tokens[0]); + } +#endif +} namespace @@ -271,11 +329,6 @@ } - bool HasCustomization() const - { - return (hasViewport_ || hasQuality_ || hasWindowing_); - } - unsigned int GetTargetWidth(unsigned int sourceWidth) const { if (hasVW_) @@ -347,9 +400,17 @@ return quality_; } - bool IsWindowing() const + bool HasWindowing() const { return hasWindowing_; + } + + void SetWindow(float center, + float width) + { + hasWindowing_ = true; + windowCenter_ = center; + windowWidth_ = width; } float GetWindowCenter() const @@ -704,15 +765,68 @@ } +static bool ReadRescale(RenderingParameters& parameters, + const Json::Value& tags) +{ + static const char* const RESCALE_INTERCEPT = "0028,1052"; + static const char* const RESCALE_SLOPE = "0028,1053"; + + if (tags.type() == Json::objectValue && + tags.isMember(RESCALE_SLOPE) && + tags.isMember(RESCALE_INTERCEPT) && + tags[RESCALE_SLOPE].type() == Json::stringValue && + tags[RESCALE_INTERCEPT].type() == Json::stringValue) + { + float s, i; + + if (ParseFloat(s, tags[RESCALE_SLOPE].asString()) && + ParseFloat(i, tags[RESCALE_INTERCEPT].asString())) + { + parameters.SetRescaleSlope(s); + parameters.SetRescaleIntercept(i); + return true; + } + } + + return false; +} + + +static bool ReadDefaultWindow(RenderingParameters& parameters, + const Json::Value& tags) +{ + static const char* const WINDOW_CENTER = "0028,1050"; + static const char* const WINDOW_WIDTH = "0028,1051"; + + if (tags.type() == Json::objectValue && + tags.isMember(WINDOW_CENTER) && + tags.isMember(WINDOW_WIDTH) && + tags[WINDOW_CENTER].type() == Json::stringValue && + tags[WINDOW_WIDTH].type() == Json::stringValue) + { + float wc, ww; + + if (ParseFirstFloat(wc, tags[WINDOW_CENTER].asString()) && + ParseFirstFloat(ww, tags[WINDOW_WIDTH].asString())) + { + parameters.SetWindow(wc, ww); + return true; + } + } + + return false; +} + static void AnswerFrameRendered(OrthancPluginRestOutput* output, std::string instanceId, int frame, const OrthancPluginHttpRequest* request) { - static const char* const RESCALE_INTERCEPT = "0028,1052"; - static const char* const RESCALE_SLOPE = "0028,1053"; static const char* const PHOTOMETRIC_INTERPRETATION = "0028,0004"; + static const char* const PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE = "5200,9230"; + static const char* const PIXEL_VALUE_TRANSFORMATION_SEQUENCE = "0028,9145"; + static const char* const FRAME_VOI_LUT_SEQUENCE = "0028,9132"; Orthanc::MimeType mime = Orthanc::MimeType_Jpeg; // This is the default in DICOMweb @@ -744,141 +858,107 @@ RenderingParameters parameters(request); OrthancPlugins::MemoryBuffer buffer; - bool badFrameError = false; - - - if (parameters.HasCustomization()) - { - if (frame <= 0) - { - badFrameError = true; - } - else - { - buffer.GetDicomInstance(instanceId); - - Json::Value tags; - buffer.DicomToJson(tags, OrthancPluginDicomToJsonFormat_Short, OrthancPluginDicomToJsonFlags_None, 255); - - if (tags.isMember(RESCALE_SLOPE) && - tags[RESCALE_SLOPE].type() == Json::stringValue) - { - try - { - parameters.SetRescaleSlope - (boost::lexical_cast - (Orthanc::Toolbox::StripSpaces(tags[RESCALE_SLOPE].asString()))); - } - catch (boost::bad_lexical_cast&) - { - } - } - - if (tags.isMember(RESCALE_INTERCEPT) && - tags[RESCALE_INTERCEPT].type() == Json::stringValue) - { - try - { - parameters.SetRescaleIntercept - (boost::lexical_cast - (Orthanc::Toolbox::StripSpaces(tags[RESCALE_INTERCEPT].asString()))); - } - catch (boost::bad_lexical_cast&) - { - } - } - - OrthancPlugins::OrthancImage dicom; - dicom.DecodeDicomImage(buffer.GetData(), buffer.GetSize(), static_cast(frame - 1)); - - Orthanc::PixelFormat targetFormat; - OrthancPluginPixelFormat sdkFormat; - if (dicom.GetPixelFormat() == OrthancPluginPixelFormat_RGB24) - { - targetFormat = Orthanc::PixelFormat_RGB24; - sdkFormat = OrthancPluginPixelFormat_RGB24; - } - else - { - targetFormat = Orthanc::PixelFormat_Grayscale8; - sdkFormat = OrthancPluginPixelFormat_Grayscale8; - } - - Orthanc::ImageAccessor source; - source.AssignReadOnly(Convert(dicom.GetPixelFormat()), - dicom.GetWidth(), dicom.GetHeight(), dicom.GetPitch(), dicom.GetBuffer()); - - Orthanc::Image target(targetFormat, parameters.GetTargetWidth(source.GetWidth()), - parameters.GetTargetHeight(source.GetHeight()), false); - - // New in 1.3: Fix for MONOCHROME1 images - bool invert = false; - if (target.GetFormat() == Orthanc::PixelFormat_Grayscale8 && - tags.isMember(PHOTOMETRIC_INTERPRETATION) && - tags[PHOTOMETRIC_INTERPRETATION].type() == Json::stringValue) - { - std::string s = tags[PHOTOMETRIC_INTERPRETATION].asString(); - Orthanc::Toolbox::StripSpaces(s); - if (s == "MONOCHROME1") - { - invert = true; - } - } - - ApplyRendering(target, source, parameters, invert); - - switch (mime) - { - case Orthanc::MimeType_Png: - OrthancPluginCompressAndAnswerPngImage(OrthancPlugins::GetGlobalContext(), output, sdkFormat, - target.GetWidth(), target.GetHeight(), target.GetPitch(), target.GetBuffer()); - break; - - case Orthanc::MimeType_Jpeg: - OrthancPluginCompressAndAnswerJpegImage(OrthancPlugins::GetGlobalContext(), output, sdkFormat, - target.GetWidth(), target.GetHeight(), target.GetPitch(), target.GetBuffer(), - parameters.GetQuality()); - break; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - } - } - else - { - // No customization of the rendering. Return the default - // preview of Orthanc. - std::map headers; - headers["Accept"] = Orthanc::EnumerationToString(mime); - - /** - * (1) In DICOMweb, the "frame" parameter is in the range [1..N], - * whereas Orthanc uses range [0..N-1], hence the "-1" below. - * - * (2) We can use "/rendered" that was introduced in the REST API - * of Orthanc 1.6.0, as since release 1.2 of the DICOMweb plugin, - * the minimal SDK version is Orthanc 1.7.0 (in order to be able - * to use transcoding primitives). In releases <= 1.2, "/preview" - * was used, which caused one issue: - * https://groups.google.com/d/msg/orthanc-users/mKgr2QAKTCU/R7u4I1LvBAAJ - **/ - if (buffer.RestApiGet("/instances/" + instanceId + "/frames/" + - boost::lexical_cast(frame - 1) + "/rendered", headers, false)) - { - OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, buffer.GetData(), - buffer.GetSize(), Orthanc::EnumerationToString(mime)); - } - else - { - badFrameError = true; - } - } - - if (badFrameError) + + if (frame <= 0) { throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, "Inexistent frame index in this image: " + boost::lexical_cast(frame)); + } + else + { + buffer.GetDicomInstance(instanceId); + + Json::Value tags; + buffer.DicomToJson(tags, OrthancPluginDicomToJsonFormat_Short, OrthancPluginDicomToJsonFlags_None, 255); + + const unsigned int f = static_cast(frame - 1); + + if (ReadRescale(parameters, tags)) + { + } + else if (tags.isMember(PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE) && + tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE].type() == Json::arrayValue && + static_cast(f) < tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE].size() && + tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE][f].type() == Json::objectValue && + tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE][f].isMember(PIXEL_VALUE_TRANSFORMATION_SEQUENCE) && + tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE][f][PIXEL_VALUE_TRANSFORMATION_SEQUENCE].type() == Json::arrayValue && + tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE][f][PIXEL_VALUE_TRANSFORMATION_SEQUENCE].size() == 1) + { + ReadRescale(parameters, tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE][f][PIXEL_VALUE_TRANSFORMATION_SEQUENCE][0]); + } + + if (!parameters.HasWindowing()) + { + if (ReadDefaultWindow(parameters, tags)) + { + } + else if (tags.isMember(PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE) && + tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE].type() == Json::arrayValue && + static_cast(f) < tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE].size() && + tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE][f].type() == Json::objectValue && + tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE][f].isMember(FRAME_VOI_LUT_SEQUENCE) && + tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE][f][FRAME_VOI_LUT_SEQUENCE].type() == Json::arrayValue && + tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE][f][FRAME_VOI_LUT_SEQUENCE].size() == 1) + { + ReadDefaultWindow(parameters, tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE][f][FRAME_VOI_LUT_SEQUENCE][0]); + } + } + + OrthancPlugins::OrthancImage dicom; + dicom.DecodeDicomImage(buffer.GetData(), buffer.GetSize(), f); + + Orthanc::PixelFormat targetFormat; + OrthancPluginPixelFormat sdkFormat; + if (dicom.GetPixelFormat() == OrthancPluginPixelFormat_RGB24) + { + targetFormat = Orthanc::PixelFormat_RGB24; + sdkFormat = OrthancPluginPixelFormat_RGB24; + } + else + { + targetFormat = Orthanc::PixelFormat_Grayscale8; + sdkFormat = OrthancPluginPixelFormat_Grayscale8; + } + + Orthanc::ImageAccessor source; + source.AssignReadOnly(Convert(dicom.GetPixelFormat()), + dicom.GetWidth(), dicom.GetHeight(), dicom.GetPitch(), dicom.GetBuffer()); + + Orthanc::Image target(targetFormat, parameters.GetTargetWidth(source.GetWidth()), + parameters.GetTargetHeight(source.GetHeight()), false); + + // New in 1.3: Fix for MONOCHROME1 images + bool invert = false; + if (target.GetFormat() == Orthanc::PixelFormat_Grayscale8 && + tags.isMember(PHOTOMETRIC_INTERPRETATION) && + tags[PHOTOMETRIC_INTERPRETATION].type() == Json::stringValue) + { + std::string s = tags[PHOTOMETRIC_INTERPRETATION].asString(); + Orthanc::Toolbox::StripSpaces(s); + if (s == "MONOCHROME1") + { + invert = true; + } + } + + ApplyRendering(target, source, parameters, invert); + + switch (mime) + { + case Orthanc::MimeType_Png: + OrthancPluginCompressAndAnswerPngImage(OrthancPlugins::GetGlobalContext(), output, sdkFormat, + target.GetWidth(), target.GetHeight(), target.GetPitch(), target.GetBuffer()); + break; + + case Orthanc::MimeType_Jpeg: + OrthancPluginCompressAndAnswerJpegImage(OrthancPlugins::GetGlobalContext(), output, sdkFormat, + target.GetWidth(), target.GetHeight(), target.GetPitch(), target.GetBuffer(), + parameters.GetQuality()); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } } } diff --git a/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake b/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake index d6983bd..29bb0e4 100644 --- a/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake +++ b/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake @@ -130,6 +130,14 @@ set(ORTHANC_FRAMEWORK_MD5 "3ea66c09f64aca990016683b6375734e") elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.3") set(ORTHANC_FRAMEWORK_MD5 "9b86e6f00e03278293cd15643cc0233f") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.4") + set(ORTHANC_FRAMEWORK_MD5 "6d5ca4a73ac7d42445041ca79de1624d") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.5") + set(ORTHANC_FRAMEWORK_MD5 "10fc64de1254a095e5d3ed3931f0cfbb") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.6") + set(ORTHANC_FRAMEWORK_MD5 "4b5d05683d747c29b2860ad79d11e62e") + elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.7") + set(ORTHANC_FRAMEWORK_MD5 "c912bbb860d640d3ae3003b5c9698205") # Below this point are development snapshots that were used to # release some plugin, before an official release of the Orthanc