Mercurial > hg > orthanc
changeset 3682:5f64c866108a
merging implementations of ImageProcessing::ShiftScale() and ApplyWindowing()
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Mon, 24 Feb 2020 16:26:59 +0100 |
parents | 453c0ece560a |
children | 12253ddefe5a |
files | Core/Images/ImageProcessing.cpp Core/Images/ImageProcessing.h UnitTestsSources/ImageProcessingTests.cpp |
diffstat | 3 files changed, 176 insertions(+), 91 deletions(-) [+] |
line wrap: on
line diff
--- a/Core/Images/ImageProcessing.cpp Thu Feb 20 20:09:24 2020 +0100 +++ b/Core/Images/ImageProcessing.cpp Mon Feb 24 16:26:59 2020 +0100 @@ -41,11 +41,11 @@ #ifdef __EMSCRIPTEN__ /* -Avoid this error: ------------------ -.../boost/math/special_functions/round.hpp:118:12: warning: implicit conversion from 'std::__2::numeric_limits<long long>::type' (aka 'long long') to 'float' changes value from 9223372036854775807 to 9223372036854775808 [-Wimplicit-int-float-conversion] -.../mnt/c/osi/dev/orthanc/Core/Images/ImageProcessing.cpp:333:28: note: in instantiation of function template specialization 'boost::math::llround<float>' requested here -.../mnt/c/osi/dev/orthanc/Core/Images/ImageProcessing.cpp:1006:9: note: in instantiation of function template specialization 'Orthanc::MultiplyConstantInternal<unsigned char, true>' requested here + Avoid this error: + ----------------- + .../boost/math/special_functions/round.hpp:118:12: warning: implicit conversion from 'std::__2::numeric_limits<long long>::type' (aka 'long long') to 'float' changes value from 9223372036854775807 to 9223372036854775808 [-Wimplicit-int-float-conversion] + .../mnt/c/osi/dev/orthanc/Core/Images/ImageProcessing.cpp:333:28: note: in instantiation of function template specialization 'boost::math::llround<float>' requested here + .../mnt/c/osi/dev/orthanc/Core/Images/ImageProcessing.cpp:1006:9: note: in instantiation of function template specialization 'Orthanc::MultiplyConstantInternal<unsigned char, true>' requested here */ #pragma GCC diagnostic ignored "-Wimplicit-int-float-conversion" #endif @@ -385,28 +385,47 @@ } - template <typename PixelType, - bool UseRound> - static void ShiftScaleInternal(ImageAccessor& image, - float offset, - float scaling, - const PixelType LowestValue = std::numeric_limits<PixelType>::min()) + // Computes "a * x + b" at each pixel => Note that this is not the + // same convention as in "ShiftScale()" + template <typename TargetType, + typename SourceType, + bool UseRound, + bool Invert> + static void ShiftScaleInternal(ImageAccessor& target, + const ImageAccessor& source, + float a, + float b, + const TargetType LowestValue) + // This function can be applied inplace (source == target) { - const PixelType minPixelValue = LowestValue; - const PixelType maxPixelValue = std::numeric_limits<PixelType>::max(); + if (source.GetWidth() != target.GetWidth() || + source.GetHeight() != target.GetHeight()) + { + throw OrthancException(ErrorCode_IncompatibleImageSize); + } + + if (&source == &target && + source.GetFormat() != target.GetFormat()) + { + throw OrthancException(ErrorCode_IncompatibleImageFormat); + } + + const TargetType minPixelValue = LowestValue; + const TargetType maxPixelValue = std::numeric_limits<TargetType>::max(); const float minFloatValue = static_cast<float>(LowestValue); const float maxFloatValue = static_cast<float>(maxPixelValue); - const unsigned int height = image.GetHeight(); - const unsigned int width = image.GetWidth(); + const unsigned int height = target.GetHeight(); + const unsigned int width = target.GetWidth(); for (unsigned int y = 0; y < height; y++) { - PixelType* p = reinterpret_cast<PixelType*>(image.GetRow(y)); + TargetType* p = reinterpret_cast<TargetType*>(target.GetRow(y)); + const SourceType* q = reinterpret_cast<const SourceType*>(source.GetRow(y)); - for (unsigned int x = 0; x < width; x++, p++) + for (unsigned int x = 0; x < width; x++, p++, q++) { - float v = (static_cast<float>(*p) + offset) * scaling; + float v = a * static_cast<float>(*q) + b; if (v >= maxFloatValue) { @@ -419,11 +438,16 @@ else if (UseRound) { // The "round" operation is very costly - *p = static_cast<PixelType>(boost::math::iround(v)); + *p = static_cast<TargetType>(boost::math::iround(v)); } else { - *p = static_cast<PixelType>(v); + *p = static_cast<TargetType>(std::floor(v)); + } + + if (Invert) + { + *p = maxPixelValue - *p; } } } @@ -498,47 +522,29 @@ float rescaleIntercept, bool invert) { + assert(sizeof(SourceType) == source.GetBytesPerPixel() && + sizeof(TargetType) == target.GetBytesPerPixel()); + // WARNING - "::min()" should be replaced by "::lowest()" if // dealing with float or double (which is not the case so far) assert(sizeof(TargetType) <= 2); // Safeguard to remember about "float/double" const TargetType minTargetValue = std::numeric_limits<TargetType>::min(); const TargetType maxTargetValue = std::numeric_limits<TargetType>::max(); - const float minFloatValue = static_cast<float>(minTargetValue); const float maxFloatValue = static_cast<float>(maxTargetValue); - + const float windowIntercept = windowCenter - windowWidth / 2.0f; const float windowSlope = (maxFloatValue + 1.0f) / windowWidth; - const unsigned int width = source.GetWidth(); - const unsigned int height = source.GetHeight(); - - for (unsigned int y = 0; y < height; y++) - { - TargetType* t = reinterpret_cast<TargetType*>(target.GetRow(y)); - const SourceType* s = reinterpret_cast<const SourceType*>(source.GetConstRow(y)); + const float a = rescaleSlope * windowSlope; + const float b = (rescaleIntercept - windowIntercept) * windowSlope; - for (unsigned int x = 0; x < width; x++, t++, s++) - { - float rescaledValue = *s * rescaleSlope + rescaleIntercept; - float v = (rescaledValue - windowIntercept) * windowSlope; - if (v <= minFloatValue) - { - v = minFloatValue; - } - else if (v >= maxFloatValue) - { - v = maxFloatValue; - } - - TargetType vv = static_cast<TargetType>(v); - - if (invert) - { - vv = maxTargetValue - vv; - } - - *t = static_cast<TargetType>(vv); - } + if (invert) + { + ShiftScaleInternal<TargetType, SourceType, false, true>(target, source, a, b, minTargetValue); + } + else + { + ShiftScaleInternal<TargetType, SourceType, false, false>(target, source, a, b, minTargetValue); } } @@ -562,42 +568,42 @@ { switch (target.GetFormat()) { - case Orthanc::PixelFormat_Grayscale8: - ApplyWindowingInternal<uint8_t, float>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); - break; - case Orthanc::PixelFormat_Grayscale16: - ApplyWindowingInternal<uint16_t, float>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); - break; - default: - throw OrthancException(ErrorCode_NotImplemented); + case Orthanc::PixelFormat_Grayscale8: + ApplyWindowingInternal<uint8_t, float>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); + break; + case Orthanc::PixelFormat_Grayscale16: + ApplyWindowingInternal<uint16_t, float>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); + break; + default: + throw OrthancException(ErrorCode_NotImplemented); } };break; case Orthanc::PixelFormat_Grayscale8: { switch (target.GetFormat()) { - case Orthanc::PixelFormat_Grayscale8: - ApplyWindowingInternal<uint8_t, uint8_t>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); - break; - case Orthanc::PixelFormat_Grayscale16: - ApplyWindowingInternal<uint16_t, uint8_t>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); - break; - default: - throw OrthancException(ErrorCode_NotImplemented); + case Orthanc::PixelFormat_Grayscale8: + ApplyWindowingInternal<uint8_t, uint8_t>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); + break; + case Orthanc::PixelFormat_Grayscale16: + ApplyWindowingInternal<uint16_t, uint8_t>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); + break; + default: + throw OrthancException(ErrorCode_NotImplemented); } };break; case Orthanc::PixelFormat_Grayscale16: { switch (target.GetFormat()) { - case Orthanc::PixelFormat_Grayscale8: - ApplyWindowingInternal<uint8_t, uint16_t>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); - break; - case Orthanc::PixelFormat_Grayscale16: - ApplyWindowingInternal<uint16_t, uint16_t>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); - break; - default: - throw OrthancException(ErrorCode_NotImplemented); + case Orthanc::PixelFormat_Grayscale8: + ApplyWindowingInternal<uint8_t, uint16_t>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); + break; + case Orthanc::PixelFormat_Grayscale16: + ApplyWindowingInternal<uint16_t, uint16_t>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert); + break; + default: + throw OrthancException(ErrorCode_NotImplemented); } };break; default: @@ -1181,7 +1187,7 @@ ShiftRightInternal<uint16_t>(image, shift); break; } - default: + default: throw OrthancException(ErrorCode_NotImplemented); } } @@ -1210,7 +1216,7 @@ ShiftLeftInternal<uint16_t>(image, shift); break; } - default: + default: throw OrthancException(ErrorCode_NotImplemented); } } @@ -1366,49 +1372,55 @@ float scaling, bool useRound) { + // Rewrite "(x + offset) * scaling" as "a * x + b" + + const float a = scaling; + const float b = offset * scaling; + switch (image.GetFormat()) { case PixelFormat_Grayscale8: if (useRound) { - ShiftScaleInternal<uint8_t, true>(image, offset, scaling); + ShiftScaleInternal<uint8_t, uint8_t, true, false>(image, image, a, b, std::numeric_limits<uint8_t>::min()); } else { - ShiftScaleInternal<uint8_t, false>(image, offset, scaling); + ShiftScaleInternal<uint8_t, uint8_t, false, false>(image, image, a, b, std::numeric_limits<uint8_t>::min()); } return; case PixelFormat_Grayscale16: if (useRound) { - ShiftScaleInternal<uint16_t, true>(image, offset, scaling); + ShiftScaleInternal<uint16_t, uint16_t, true, false>(image, image, a, b, std::numeric_limits<uint16_t>::min()); } else { - ShiftScaleInternal<uint16_t, false>(image, offset, scaling); + ShiftScaleInternal<uint16_t, uint16_t, false, false>(image, image, a, b, std::numeric_limits<uint16_t>::min()); } return; case PixelFormat_SignedGrayscale16: if (useRound) { - ShiftScaleInternal<int16_t, true>(image, offset, scaling); + ShiftScaleInternal<int16_t, int16_t, true, false>(image, image, a, b, std::numeric_limits<int16_t>::min()); } else { - ShiftScaleInternal<int16_t, false>(image, offset, scaling); + ShiftScaleInternal<int16_t, int16_t, false, false>(image, image, a, b, std::numeric_limits<int16_t>::min()); } return; case PixelFormat_Float32: + // "::min()" must be replaced by "::lowest()" if dealing with float or double if (useRound) { - ShiftScaleInternal<float, true>(image, offset, scaling, -std::numeric_limits<float>::max()); + ShiftScaleInternal<float, float, true, false>(image, image, a, b, std::numeric_limits<float>::lowest()); } else { - ShiftScaleInternal<float, false>(image, offset, scaling, -std::numeric_limits<float>::max()); + ShiftScaleInternal<float, float, false, false>(image, image, a, b, std::numeric_limits<float>::lowest()); } return; @@ -2216,7 +2228,7 @@ // Deal with the right border for (unsigned int x = static_cast<unsigned int>( - horizontalAnchor + width - horizontal.size() + 1); x < width; x++) + horizontalAnchor + width - horizontal.size() + 1); x < width; x++) { for (unsigned int c = 0; c < ChannelsCount; c++, p++) {
--- a/Core/Images/ImageProcessing.h Thu Feb 20 20:09:24 2020 +0100 +++ b/Core/Images/ImageProcessing.h Mon Feb 24 16:26:59 2020 +0100 @@ -127,7 +127,7 @@ float factor, bool useRound); - // "useRound" is expensive + // Computes "(x + offset) * scaling" inplace. "useRound" is expensive. void ShiftScale(ImageAccessor& image, float offset, float scaling,
--- a/UnitTestsSources/ImageProcessingTests.cpp Thu Feb 20 20:09:24 2020 +0100 +++ b/UnitTestsSources/ImageProcessingTests.cpp Mon Feb 24 16:26:59 2020 +0100 @@ -285,7 +285,7 @@ static void SetGrayscale16Pixel(ImageAccessor& image, unsigned int x, unsigned int y, - uint8_t value) + uint16_t value) { ImageTraits<PixelFormat_Grayscale16>::SetPixel(image, value, x, y); } @@ -301,6 +301,25 @@ return p == value; } +static void SetSignedGrayscale16Pixel(ImageAccessor& image, + unsigned int x, + unsigned int y, + int16_t value) +{ + ImageTraits<PixelFormat_SignedGrayscale16>::SetPixel(image, value, x, y); +} + +static bool TestSignedGrayscale16Pixel(const ImageAccessor& image, + unsigned int x, + unsigned int y, + int16_t value) +{ + PixelTraits<PixelFormat_SignedGrayscale16>::PixelType p; + ImageTraits<PixelFormat_SignedGrayscale16>::GetPixel(p, image, x, y); + if (p != value) printf("%d %d\n", p, value); + return p == value; +} + static void SetRGB24Pixel(ImageAccessor& image, unsigned int x, unsigned int y, @@ -831,7 +850,7 @@ { Image target(PixelFormat_Grayscale8, 6, 1, false); - ImageProcessing::ApplyWindowing(target, image, 5000.0f, 10000.0f, 1000.0f, 0.0f, false); + ImageProcessing::ApplyWindowing(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)); @@ -843,7 +862,7 @@ { Image target(PixelFormat_Grayscale8, 6, 1, false); - ImageProcessing::ApplyWindowing(target, image, 5000.0f, 10000.0f, 1000.0f, 0.0f, true); + ImageProcessing::ApplyWindowing(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)); @@ -855,7 +874,7 @@ { Image target(PixelFormat_Grayscale8, 6, 1, false); - ImageProcessing::ApplyWindowing(target, image, 50.0f, 100.0f, 10.0f, 30.0f, false); + ImageProcessing::ApplyWindowing(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% @@ -938,3 +957,57 @@ } } } + + +TEST(ImageProcessing, ShiftScaleGrayscale8) +{ + Image image(PixelFormat_Grayscale8, 5, 1, false); + SetGrayscale8Pixel(image, 0, 0, 0); + SetGrayscale8Pixel(image, 1, 0, 2); + SetGrayscale8Pixel(image, 2, 0, 5); + SetGrayscale8Pixel(image, 3, 0, 10); + SetGrayscale8Pixel(image, 4, 0, 255); + + ImageProcessing::ShiftScale(image, -1.1, 1.5, true); + ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 0)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 0, 1)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 0, 6)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 3, 0, 13)); + ASSERT_TRUE(TestGrayscale8Pixel(image, 4, 0, 255)); +} + + +TEST(ImageProcessing, ShiftScaleGrayscale16) +{ + Image image(PixelFormat_Grayscale16, 5, 1, false); + SetGrayscale16Pixel(image, 0, 0, 0); + SetGrayscale16Pixel(image, 1, 0, 2); + SetGrayscale16Pixel(image, 2, 0, 5); + SetGrayscale16Pixel(image, 3, 0, 10); + SetGrayscale16Pixel(image, 4, 0, 255); + + ImageProcessing::ShiftScale(image, -1.1, 1.5, true); + ASSERT_TRUE(TestGrayscale16Pixel(image, 0, 0, 0)); + ASSERT_TRUE(TestGrayscale16Pixel(image, 1, 0, 1)); + ASSERT_TRUE(TestGrayscale16Pixel(image, 2, 0, 6)); + ASSERT_TRUE(TestGrayscale16Pixel(image, 3, 0, 13)); + ASSERT_TRUE(TestGrayscale16Pixel(image, 4, 0, 381)); +} + + +TEST(ImageProcessing, ShiftScaleSignedGrayscale16) +{ + Image image(PixelFormat_SignedGrayscale16, 5, 1, false); + SetSignedGrayscale16Pixel(image, 0, 0, 0); + SetSignedGrayscale16Pixel(image, 1, 0, 2); + SetSignedGrayscale16Pixel(image, 2, 0, 5); + SetSignedGrayscale16Pixel(image, 3, 0, 10); + SetSignedGrayscale16Pixel(image, 4, 0, 255); + + ImageProcessing::ShiftScale(image, -17.1, 11.5, true); + ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 0, 0, -197)); + ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 1, 0, -174)); + ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 2, 0, -139)); + ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 3, 0, -82)); + ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 4, 0, 2736)); +}