27 | 27 |
#include <Logging.h>
|
28 | 28 |
#include <Toolbox.h>
|
29 | 29 |
|
|
30 |
#if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 9, 7)
|
|
31 |
# include <SerializationToolbox.h>
|
|
32 |
#else
|
|
33 |
# include <boost/lexical_cast.hpp>
|
|
34 |
#endif
|
|
35 |
|
30 | 36 |
#include <boost/algorithm/string/predicate.hpp>
|
31 | 37 |
#include <boost/math/special_functions/round.hpp>
|
|
38 |
|
|
39 |
|
|
40 |
static bool ParseFloat(float& target,
|
|
41 |
const std::string& source)
|
|
42 |
{
|
|
43 |
#if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 9, 7)
|
|
44 |
return Orthanc::SerializationToolbox::ParseFloat(target, source);
|
|
45 |
|
|
46 |
#else
|
|
47 |
// Emulation for older versions of the Orthanc framework
|
|
48 |
std::string s = Orthanc::Toolbox::StripSpaces(source);
|
|
49 |
|
|
50 |
if (s.empty())
|
|
51 |
{
|
|
52 |
return false;
|
|
53 |
}
|
|
54 |
else
|
|
55 |
{
|
|
56 |
try
|
|
57 |
{
|
|
58 |
target = boost::lexical_cast<float>(s);
|
|
59 |
return true;
|
|
60 |
}
|
|
61 |
catch (boost::bad_lexical_cast&)
|
|
62 |
{
|
|
63 |
return false;
|
|
64 |
}
|
|
65 |
}
|
|
66 |
#endif
|
|
67 |
}
|
|
68 |
|
|
69 |
|
|
70 |
static bool ParseFirstFloat(float& target,
|
|
71 |
const std::string& source)
|
|
72 |
{
|
|
73 |
#if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 9, 7)
|
|
74 |
return Orthanc::SerializationToolbox::ParseFirstFloat(target, source);
|
|
75 |
|
|
76 |
#else
|
|
77 |
// Emulation for older versions of the Orthanc framework
|
|
78 |
std::vector<std::string> tokens;
|
|
79 |
Orthanc::Toolbox::TokenizeString(tokens, source, '\\');
|
|
80 |
if (tokens.empty())
|
|
81 |
{
|
|
82 |
return false;
|
|
83 |
}
|
|
84 |
else
|
|
85 |
{
|
|
86 |
return ParseFloat(target, tokens[0]);
|
|
87 |
}
|
|
88 |
#endif
|
|
89 |
}
|
32 | 90 |
|
33 | 91 |
|
34 | 92 |
namespace
|
|
270 | 328 |
}
|
271 | 329 |
|
272 | 330 |
|
273 | |
bool HasCustomization() const
|
274 | |
{
|
275 | |
return (hasViewport_ || hasQuality_ || hasWindowing_);
|
276 | |
}
|
277 | |
|
278 | 331 |
unsigned int GetTargetWidth(unsigned int sourceWidth) const
|
279 | 332 |
{
|
280 | 333 |
if (hasVW_)
|
|
346 | 399 |
return quality_;
|
347 | 400 |
}
|
348 | 401 |
|
349 | |
bool IsWindowing() const
|
|
402 |
bool HasWindowing() const
|
350 | 403 |
{
|
351 | 404 |
return hasWindowing_;
|
|
405 |
}
|
|
406 |
|
|
407 |
void SetWindow(float center,
|
|
408 |
float width)
|
|
409 |
{
|
|
410 |
hasWindowing_ = true;
|
|
411 |
windowCenter_ = center;
|
|
412 |
windowWidth_ = width;
|
352 | 413 |
}
|
353 | 414 |
|
354 | 415 |
float GetWindowCenter() const
|
|
703 | 764 |
}
|
704 | 765 |
|
705 | 766 |
|
|
767 |
static bool ReadRescale(RenderingParameters& parameters,
|
|
768 |
const Json::Value& tags)
|
|
769 |
{
|
|
770 |
static const char* const RESCALE_INTERCEPT = "0028,1052";
|
|
771 |
static const char* const RESCALE_SLOPE = "0028,1053";
|
|
772 |
|
|
773 |
if (tags.type() == Json::objectValue &&
|
|
774 |
tags.isMember(RESCALE_SLOPE) &&
|
|
775 |
tags.isMember(RESCALE_INTERCEPT) &&
|
|
776 |
tags[RESCALE_SLOPE].type() == Json::stringValue &&
|
|
777 |
tags[RESCALE_INTERCEPT].type() == Json::stringValue)
|
|
778 |
{
|
|
779 |
float s, i;
|
|
780 |
|
|
781 |
if (ParseFloat(s, tags[RESCALE_SLOPE].asString()) &&
|
|
782 |
ParseFloat(i, tags[RESCALE_INTERCEPT].asString()))
|
|
783 |
{
|
|
784 |
parameters.SetRescaleSlope(s);
|
|
785 |
parameters.SetRescaleIntercept(i);
|
|
786 |
return true;
|
|
787 |
}
|
|
788 |
}
|
|
789 |
|
|
790 |
return false;
|
|
791 |
}
|
|
792 |
|
|
793 |
|
|
794 |
static bool ReadDefaultWindow(RenderingParameters& parameters,
|
|
795 |
const Json::Value& tags)
|
|
796 |
{
|
|
797 |
static const char* const WINDOW_CENTER = "0028,1050";
|
|
798 |
static const char* const WINDOW_WIDTH = "0028,1051";
|
|
799 |
|
|
800 |
if (tags.type() == Json::objectValue &&
|
|
801 |
tags.isMember(WINDOW_CENTER) &&
|
|
802 |
tags.isMember(WINDOW_WIDTH) &&
|
|
803 |
tags[WINDOW_CENTER].type() == Json::stringValue &&
|
|
804 |
tags[WINDOW_WIDTH].type() == Json::stringValue)
|
|
805 |
{
|
|
806 |
float wc, ww;
|
|
807 |
|
|
808 |
if (ParseFirstFloat(wc, tags[WINDOW_CENTER].asString()) &&
|
|
809 |
ParseFirstFloat(ww, tags[WINDOW_WIDTH].asString()))
|
|
810 |
{
|
|
811 |
parameters.SetWindow(wc, ww);
|
|
812 |
return true;
|
|
813 |
}
|
|
814 |
}
|
|
815 |
|
|
816 |
return false;
|
|
817 |
}
|
|
818 |
|
706 | 819 |
|
707 | 820 |
static void AnswerFrameRendered(OrthancPluginRestOutput* output,
|
708 | 821 |
std::string instanceId,
|
709 | 822 |
int frame,
|
710 | 823 |
const OrthancPluginHttpRequest* request)
|
711 | 824 |
{
|
712 | |
static const char* const RESCALE_INTERCEPT = "0028,1052";
|
713 | |
static const char* const RESCALE_SLOPE = "0028,1053";
|
714 | 825 |
static const char* const PHOTOMETRIC_INTERPRETATION = "0028,0004";
|
|
826 |
static const char* const PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE = "5200,9230";
|
|
827 |
static const char* const PIXEL_VALUE_TRANSFORMATION_SEQUENCE = "0028,9145";
|
|
828 |
static const char* const FRAME_VOI_LUT_SEQUENCE = "0028,9132";
|
715 | 829 |
|
716 | 830 |
Orthanc::MimeType mime = Orthanc::MimeType_Jpeg; // This is the default in DICOMweb
|
717 | 831 |
|
|
743 | 857 |
RenderingParameters parameters(request);
|
744 | 858 |
|
745 | 859 |
OrthancPlugins::MemoryBuffer buffer;
|
746 | |
bool badFrameError = false;
|
747 | |
|
748 | |
|
749 | |
if (parameters.HasCustomization())
|
750 | |
{
|
751 | |
if (frame <= 0)
|
752 | |
{
|
753 | |
badFrameError = true;
|
754 | |
}
|
755 | |
else
|
756 | |
{
|
757 | |
buffer.GetDicomInstance(instanceId);
|
758 | |
|
759 | |
Json::Value tags;
|
760 | |
buffer.DicomToJson(tags, OrthancPluginDicomToJsonFormat_Short, OrthancPluginDicomToJsonFlags_None, 255);
|
761 | |
|
762 | |
if (tags.isMember(RESCALE_SLOPE) &&
|
763 | |
tags[RESCALE_SLOPE].type() == Json::stringValue)
|
764 | |
{
|
765 | |
try
|
766 | |
{
|
767 | |
parameters.SetRescaleSlope
|
768 | |
(boost::lexical_cast<float>
|
769 | |
(Orthanc::Toolbox::StripSpaces(tags[RESCALE_SLOPE].asString())));
|
770 | |
}
|
771 | |
catch (boost::bad_lexical_cast&)
|
772 | |
{
|
773 | |
}
|
774 | |
}
|
775 | |
|
776 | |
if (tags.isMember(RESCALE_INTERCEPT) &&
|
777 | |
tags[RESCALE_INTERCEPT].type() == Json::stringValue)
|
778 | |
{
|
779 | |
try
|
780 | |
{
|
781 | |
parameters.SetRescaleIntercept
|
782 | |
(boost::lexical_cast<float>
|
783 | |
(Orthanc::Toolbox::StripSpaces(tags[RESCALE_INTERCEPT].asString())));
|
784 | |
}
|
785 | |
catch (boost::bad_lexical_cast&)
|
786 | |
{
|
787 | |
}
|
788 | |
}
|
789 | |
|
790 | |
OrthancPlugins::OrthancImage dicom;
|
791 | |
dicom.DecodeDicomImage(buffer.GetData(), buffer.GetSize(), static_cast<unsigned int>(frame - 1));
|
792 | |
|
793 | |
Orthanc::PixelFormat targetFormat;
|
794 | |
OrthancPluginPixelFormat sdkFormat;
|
795 | |
if (dicom.GetPixelFormat() == OrthancPluginPixelFormat_RGB24)
|
796 | |
{
|
797 | |
targetFormat = Orthanc::PixelFormat_RGB24;
|
798 | |
sdkFormat = OrthancPluginPixelFormat_RGB24;
|
799 | |
}
|
800 | |
else
|
801 | |
{
|
802 | |
targetFormat = Orthanc::PixelFormat_Grayscale8;
|
803 | |
sdkFormat = OrthancPluginPixelFormat_Grayscale8;
|
804 | |
}
|
805 | |
|
806 | |
Orthanc::ImageAccessor source;
|
807 | |
source.AssignReadOnly(Convert(dicom.GetPixelFormat()),
|
808 | |
dicom.GetWidth(), dicom.GetHeight(), dicom.GetPitch(), dicom.GetBuffer());
|
809 | |
|
810 | |
Orthanc::Image target(targetFormat, parameters.GetTargetWidth(source.GetWidth()),
|
811 | |
parameters.GetTargetHeight(source.GetHeight()), false);
|
812 | |
|
813 | |
// New in 1.3: Fix for MONOCHROME1 images
|
814 | |
bool invert = false;
|
815 | |
if (target.GetFormat() == Orthanc::PixelFormat_Grayscale8 &&
|
816 | |
tags.isMember(PHOTOMETRIC_INTERPRETATION) &&
|
817 | |
tags[PHOTOMETRIC_INTERPRETATION].type() == Json::stringValue)
|
818 | |
{
|
819 | |
std::string s = tags[PHOTOMETRIC_INTERPRETATION].asString();
|
820 | |
Orthanc::Toolbox::StripSpaces(s);
|
821 | |
if (s == "MONOCHROME1")
|
822 | |
{
|
823 | |
invert = true;
|
824 | |
}
|
825 | |
}
|
826 | |
|
827 | |
ApplyRendering(target, source, parameters, invert);
|
828 | |
|
829 | |
switch (mime)
|
830 | |
{
|
831 | |
case Orthanc::MimeType_Png:
|
832 | |
OrthancPluginCompressAndAnswerPngImage(OrthancPlugins::GetGlobalContext(), output, sdkFormat,
|
833 | |
target.GetWidth(), target.GetHeight(), target.GetPitch(), target.GetBuffer());
|
834 | |
break;
|
835 | |
|
836 | |
case Orthanc::MimeType_Jpeg:
|
837 | |
OrthancPluginCompressAndAnswerJpegImage(OrthancPlugins::GetGlobalContext(), output, sdkFormat,
|
838 | |
target.GetWidth(), target.GetHeight(), target.GetPitch(), target.GetBuffer(),
|
839 | |
parameters.GetQuality());
|
840 | |
break;
|
841 | |
|
842 | |
default:
|
843 | |
throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
|
844 | |
}
|
845 | |
}
|
846 | |
}
|
847 | |
else
|
848 | |
{
|
849 | |
// No customization of the rendering. Return the default
|
850 | |
// preview of Orthanc.
|
851 | |
std::map<std::string, std::string> headers;
|
852 | |
headers["Accept"] = Orthanc::EnumerationToString(mime);
|
853 | |
|
854 | |
/**
|
855 | |
* (1) In DICOMweb, the "frame" parameter is in the range [1..N],
|
856 | |
* whereas Orthanc uses range [0..N-1], hence the "-1" below.
|
857 | |
*
|
858 | |
* (2) We can use "/rendered" that was introduced in the REST API
|
859 | |
* of Orthanc 1.6.0, as since release 1.2 of the DICOMweb plugin,
|
860 | |
* the minimal SDK version is Orthanc 1.7.0 (in order to be able
|
861 | |
* to use transcoding primitives). In releases <= 1.2, "/preview"
|
862 | |
* was used, which caused one issue:
|
863 | |
* https://groups.google.com/d/msg/orthanc-users/mKgr2QAKTCU/R7u4I1LvBAAJ
|
864 | |
**/
|
865 | |
if (buffer.RestApiGet("/instances/" + instanceId + "/frames/" +
|
866 | |
boost::lexical_cast<std::string>(frame - 1) + "/rendered", headers, false))
|
867 | |
{
|
868 | |
OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, buffer.GetData(),
|
869 | |
buffer.GetSize(), Orthanc::EnumerationToString(mime));
|
870 | |
}
|
871 | |
else
|
872 | |
{
|
873 | |
badFrameError = true;
|
874 | |
}
|
875 | |
}
|
876 | |
|
877 | |
if (badFrameError)
|
|
860 |
|
|
861 |
if (frame <= 0)
|
878 | 862 |
{
|
879 | 863 |
throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
|
880 | 864 |
"Inexistent frame index in this image: " + boost::lexical_cast<std::string>(frame));
|
|
865 |
}
|
|
866 |
else
|
|
867 |
{
|
|
868 |
buffer.GetDicomInstance(instanceId);
|
|
869 |
|
|
870 |
Json::Value tags;
|
|
871 |
buffer.DicomToJson(tags, OrthancPluginDicomToJsonFormat_Short, OrthancPluginDicomToJsonFlags_None, 255);
|
|
872 |
|
|
873 |
const unsigned int f = static_cast<unsigned int>(frame - 1);
|
|
874 |
|
|
875 |
if (ReadRescale(parameters, tags))
|
|
876 |
{
|
|
877 |
}
|
|
878 |
else if (tags.isMember(PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE) &&
|
|
879 |
tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE].type() == Json::arrayValue &&
|
|
880 |
static_cast<Json::Value::ArrayIndex>(f) < tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE].size() &&
|
|
881 |
tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE][f].type() == Json::objectValue &&
|
|
882 |
tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE][f].isMember(PIXEL_VALUE_TRANSFORMATION_SEQUENCE) &&
|
|
883 |
tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE][f][PIXEL_VALUE_TRANSFORMATION_SEQUENCE].type() == Json::arrayValue &&
|
|
884 |
tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE][f][PIXEL_VALUE_TRANSFORMATION_SEQUENCE].size() == 1)
|
|
885 |
{
|
|
886 |
ReadRescale(parameters, tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE][f][PIXEL_VALUE_TRANSFORMATION_SEQUENCE][0]);
|
|
887 |
}
|
|
888 |
|
|
889 |
if (!parameters.HasWindowing())
|
|
890 |
{
|
|
891 |
if (ReadDefaultWindow(parameters, tags))
|
|
892 |
{
|
|
893 |
}
|
|
894 |
else if (tags.isMember(PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE) &&
|
|
895 |
tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE].type() == Json::arrayValue &&
|
|
896 |
static_cast<Json::Value::ArrayIndex>(f) < tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE].size() &&
|
|
897 |
tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE][f].type() == Json::objectValue &&
|
|
898 |
tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE][f].isMember(FRAME_VOI_LUT_SEQUENCE) &&
|
|
899 |
tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE][f][FRAME_VOI_LUT_SEQUENCE].type() == Json::arrayValue &&
|
|
900 |
tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE][f][FRAME_VOI_LUT_SEQUENCE].size() == 1)
|
|
901 |
{
|
|
902 |
ReadDefaultWindow(parameters, tags[PER_FRAME_FUNCTIONAL_GROUPS_SEQUENCE][f][FRAME_VOI_LUT_SEQUENCE][0]);
|
|
903 |
}
|
|
904 |
}
|
|
905 |
|
|
906 |
OrthancPlugins::OrthancImage dicom;
|
|
907 |
dicom.DecodeDicomImage(buffer.GetData(), buffer.GetSize(), f);
|
|
908 |
|
|
909 |
Orthanc::PixelFormat targetFormat;
|
|
910 |
OrthancPluginPixelFormat sdkFormat;
|
|
911 |
if (dicom.GetPixelFormat() == OrthancPluginPixelFormat_RGB24)
|
|
912 |
{
|
|
913 |
targetFormat = Orthanc::PixelFormat_RGB24;
|
|
914 |
sdkFormat = OrthancPluginPixelFormat_RGB24;
|
|
915 |
}
|
|
916 |
else
|
|
917 |
{
|
|
918 |
targetFormat = Orthanc::PixelFormat_Grayscale8;
|
|
919 |
sdkFormat = OrthancPluginPixelFormat_Grayscale8;
|
|
920 |
}
|
|
921 |
|
|
922 |
Orthanc::ImageAccessor source;
|
|
923 |
source.AssignReadOnly(Convert(dicom.GetPixelFormat()),
|
|
924 |
dicom.GetWidth(), dicom.GetHeight(), dicom.GetPitch(), dicom.GetBuffer());
|
|
925 |
|
|
926 |
Orthanc::Image target(targetFormat, parameters.GetTargetWidth(source.GetWidth()),
|
|
927 |
parameters.GetTargetHeight(source.GetHeight()), false);
|
|
928 |
|
|
929 |
// New in 1.3: Fix for MONOCHROME1 images
|
|
930 |
bool invert = false;
|
|
931 |
if (target.GetFormat() == Orthanc::PixelFormat_Grayscale8 &&
|
|
932 |
tags.isMember(PHOTOMETRIC_INTERPRETATION) &&
|
|
933 |
tags[PHOTOMETRIC_INTERPRETATION].type() == Json::stringValue)
|
|
934 |
{
|
|
935 |
std::string s = tags[PHOTOMETRIC_INTERPRETATION].asString();
|
|
936 |
Orthanc::Toolbox::StripSpaces(s);
|
|
937 |
if (s == "MONOCHROME1")
|
|
938 |
{
|
|
939 |
invert = true;
|
|
940 |
}
|
|
941 |
}
|
|
942 |
|
|
943 |
ApplyRendering(target, source, parameters, invert);
|
|
944 |
|
|
945 |
switch (mime)
|
|
946 |
{
|
|
947 |
case Orthanc::MimeType_Png:
|
|
948 |
OrthancPluginCompressAndAnswerPngImage(OrthancPlugins::GetGlobalContext(), output, sdkFormat,
|
|
949 |
target.GetWidth(), target.GetHeight(), target.GetPitch(), target.GetBuffer());
|
|
950 |
break;
|
|
951 |
|
|
952 |
case Orthanc::MimeType_Jpeg:
|
|
953 |
OrthancPluginCompressAndAnswerJpegImage(OrthancPlugins::GetGlobalContext(), output, sdkFormat,
|
|
954 |
target.GetWidth(), target.GetHeight(), target.GetPitch(), target.GetBuffer(),
|
|
955 |
parameters.GetQuality());
|
|
956 |
break;
|
|
957 |
|
|
958 |
default:
|
|
959 |
throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
|
|
960 |
}
|
881 | 961 |
}
|
882 | 962 |
}
|
883 | 963 |
|