diff --git a/CMakeLists.txt b/CMakeLists.txt index 52cbebd87..c7873928a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -335,3 +335,8 @@ set(MAGNUM_PLUGINS_AUDIOIMPORTER_RELEASE_DIR ${MAGNUM_PLUGINS_RELEASE_DIR}/audio add_subdirectory(modules) add_subdirectory(src) + +# Build snippets as part of testing +if(BUILD_TESTS) + add_subdirectory(doc/snippets) +endif() diff --git a/Doxyfile b/Doxyfile index 3b466f962..f8c6aada1 100644 --- a/Doxyfile +++ b/Doxyfile @@ -892,7 +892,8 @@ EXCLUDE_SYMBOLS = Magnum::*Implementation \ # that contain example code fragments that are included (see the \include # command). -EXAMPLE_PATH = ../magnum-examples/src/ +EXAMPLE_PATH = doc/snippets/ \ + ../magnum-examples/src/ # If the value of the EXAMPLE_PATH tag contains directories, you can use the # EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and @@ -913,6 +914,7 @@ EXAMPLE_RECURSIVE = NO # \image command). IMAGE_PATH = doc/ \ + doc/snippets/ \ ../magnum-examples/src/ # The INPUT_FILTER tag can be used to specify a program that doxygen should diff --git a/doc/snippets/CMakeLists.txt b/doc/snippets/CMakeLists.txt new file mode 100644 index 000000000..81e8fcbe3 --- /dev/null +++ b/doc/snippets/CMakeLists.txt @@ -0,0 +1,40 @@ +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016 +# Vladimír Vondruš +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +find_package(Corrade COMPONENTS TestSuite) + +if(WITH_DEBUGTOOLS AND Corrade_TestSuite_FOUND) + set(SNIPPETS_DIR ${CMAKE_CURRENT_SOURCE_DIR}) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configure.h.cmake + ${CMAKE_CURRENT_BINARY_DIR}/configure.h) + + # CompareImage documentation snippet. I need it executable so I can + # copy&paste the output to the documentation. Also mot using + # corrade_add_test() because it shouldn't be run as part of CTest as it + # purposedly fails. + add_executable(debugtools-compareimage debugtools-compareimage.cpp) + target_link_libraries(debugtools-compareimage PRIVATE MagnumDebugTools) + target_include_directories(debugtools-compareimage PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +endif() diff --git a/doc/snippets/configure.h.cmake b/doc/snippets/configure.h.cmake new file mode 100644 index 000000000..4ecdcf137 --- /dev/null +++ b/doc/snippets/configure.h.cmake @@ -0,0 +1,27 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016 + Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#define MAGNUM_PLUGINS_IMPORTER_DIR "${MAGNUM_PLUGINS_IMPORTER_DIR}" +#define SNIPPETS_DIR "${SNIPPETS_DIR}" diff --git a/doc/snippets/debugtools-compareimage.cpp b/doc/snippets/debugtools-compareimage.cpp new file mode 100644 index 000000000..70fdc58c7 --- /dev/null +++ b/doc/snippets/debugtools-compareimage.cpp @@ -0,0 +1,82 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016 + Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include + +#include "Magnum/Image.h" +#include "Magnum/Trade/ImageData.h" +#include "Magnum/Trade/AbstractImporter.h" +#include "Magnum/DebugTools/CompareImage.h" + +#include "configure.h" + +using namespace Magnum; + +namespace { + +Image2D doProcessing() { + PluginManager::Manager manager{MAGNUM_PLUGINS_IMPORTER_DIR}; + std::unique_ptr importer = manager.loadAndInstantiate("TgaImporter"); + importer->openFile(Utility::Directory::join(SNIPPETS_DIR, "image2.tga")); + auto image = importer->image2D(0); + CORRADE_INTERNAL_ASSERT(image); + return Image2D{image->storage(), image->format(), image->type(), image->size(), image->release()}; +} + +Image2D loadExpectedImage() { + PluginManager::Manager manager{MAGNUM_PLUGINS_IMPORTER_DIR}; + std::unique_ptr importer = manager.loadAndInstantiate("TgaImporter"); + importer->openFile(Utility::Directory::join(SNIPPETS_DIR, "image1.tga")); + auto image = importer->image2D(0); + CORRADE_INTERNAL_ASSERT(image); + return Image2D{image->storage(), image->format(), image->type(), image->size(), image->release()}; +} + +} + +struct ProcessingTest: TestSuite::Tester { + explicit ProcessingTest(); + + void process(); +}; + +ProcessingTest::ProcessingTest() { + addTests({&ProcessingTest::process}); +} + +/** [0] */ +void ProcessingTest::process() { + Image2D actual = doProcessing(); + Image2D expected = loadExpectedImage(); + + CORRADE_COMPARE_WITH(actual, expected, + (DebugTools::CompareImage{170.0f, 96.0f})); +} +/** [0] */ + +CORRADE_TEST_MAIN(ProcessingTest) + diff --git a/doc/snippets/debugtools-compareimage.png b/doc/snippets/debugtools-compareimage.png new file mode 100644 index 000000000..b2a418d80 Binary files /dev/null and b/doc/snippets/debugtools-compareimage.png differ diff --git a/doc/snippets/image1.tga b/doc/snippets/image1.tga new file mode 100644 index 000000000..8717679bd Binary files /dev/null and b/doc/snippets/image1.tga differ diff --git a/doc/snippets/image2.tga b/doc/snippets/image2.tga new file mode 100644 index 000000000..b3190f05f Binary files /dev/null and b/doc/snippets/image2.tga differ diff --git a/src/Magnum/DebugTools/CMakeLists.txt b/src/Magnum/DebugTools/CMakeLists.txt index 9394ab0fb..4393ecd8c 100644 --- a/src/Magnum/DebugTools/CMakeLists.txt +++ b/src/Magnum/DebugTools/CMakeLists.txt @@ -43,6 +43,16 @@ if(MAGNUM_TARGET_GLES AND NOT MAGNUM_TARGET_GLES2) list(APPEND MagnumDebugTools_SRCS ${MagnumDebugTools_RESOURCES}) endif() +# Build the TestSuite-related functionality only if it is present +find_package(Corrade COMPONENTS TestSuite) +if(Corrade_TestSuite_FOUND) + list(APPEND MagnumDebugTools_SRCS + CompareImage.cpp) + + list(APPEND MagnumDebugTools_HEADERS + CompareImage.h) +endif() + if(NOT MAGNUM_TARGET_WEBGL) list(APPEND MagnumDebugTools_SRCS BufferData.cpp) @@ -107,6 +117,9 @@ if(BUILD_STATIC_PIC) endif() target_link_libraries(MagnumDebugTools Magnum) +if(Corrade_TestSuite_FOUND) + target_link_libraries(MagnumDebugTools Corrade::TestSuite) +endif() if(WITH_SCENEGRAPH) target_link_libraries(MagnumDebugTools MagnumSceneGraph) endif() diff --git a/src/Magnum/DebugTools/CompareImage.cpp b/src/Magnum/DebugTools/CompareImage.cpp new file mode 100644 index 000000000..f11b85ca8 --- /dev/null +++ b/src/Magnum/DebugTools/CompareImage.cpp @@ -0,0 +1,507 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016 + Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include "CompareImage.h" + +#include +#include +#include + +#include "Magnum/ImageView.h" +#include "Magnum/PixelFormat.h" +#include "Magnum/Math/Functions.h" +#include "Magnum/Math/Color.h" +#include "Magnum/Math/Algorithms/KahanSum.h" + +namespace Magnum { namespace DebugTools { namespace Implementation { + +namespace { + +template Math::Vector pixelAt(const char* const pixels, const std::size_t stride, const Vector2i& pos) { + return reinterpret_cast*>(pixels + stride*pos.y())[pos.x()]; +} + +template Float calculateImageDelta(const ImageView2D& actual, const ImageView2D& expected, std::vector& output) { + CORRADE_INTERNAL_ASSERT(output.size() == std::size_t(expected.size().product())); + + /* Precalculate parameters for pixel access */ + Math::Vector2 dataOffset, dataSize; + + std::tie(dataOffset, dataSize, std::ignore) = actual.dataProperties(); + const char* const actualPixels = actual.data() + dataOffset.sum(); + const std::size_t actualStride = dataSize.x(); + + std::tie(dataOffset, dataSize, std::ignore) = expected.dataProperties(); + const char* const expectedPixels = expected.data() + dataOffset.sum(); + const std::size_t expectedStride = dataSize.x(); + + /* Calculate deltas and maximal value of them */ + Float max{}; + for(std::int_fast32_t y = 0; y != expected.size().y(); ++y) { + for(std::int_fast32_t x = 0; x != expected.size().x(); ++x) { + Math::Vector actualPixel{pixelAt(actualPixels, actualStride, {Int(x), Int(y)})}; + Math::Vector expectedPixel{pixelAt(expectedPixels, expectedStride, {Int(x), Int(y)})}; + + const Float value = (Math::abs(actualPixel - expectedPixel)).sum()/size; + output[y*expected.size().x() + x] = value; + max = Math::max(max, value); + } + } + + return max; +} + +template Float calculateIntegerImageDelta(const ImageView2D& actual, const ImageView2D& expected, std::vector& output) { + if( + #if !(defined(MAGNUM_TARGET_WEBGL) && defined(MAGNUM_TARGET_GLES2)) + expected.format() == PixelFormat::Red + #endif + #ifndef MAGNUM_TARGET_GLES2 + || expected.format() == PixelFormat::RedInteger + #else + #ifndef MAGNUM_TARGET_WEBGL + || + #endif + expected.format() == PixelFormat::Luminance + #endif + ) + return calculateImageDelta<1, T>(actual, expected, output); + else if( + #if !(defined(MAGNUM_TARGET_WEBGL) && defined(MAGNUM_TARGET_GLES2)) + expected.format() == PixelFormat::RG + #endif + #ifndef MAGNUM_TARGET_GLES2 + || expected.format() == PixelFormat::RGInteger + #else + #ifndef MAGNUM_TARGET_WEBGL + || + #endif + expected.format() == PixelFormat::LuminanceAlpha + #endif + ) + return calculateImageDelta<2, T>(actual, expected, output); + else if(expected.format() == PixelFormat::RGB + #ifndef MAGNUM_TARGET_GLES2 + || expected.format() == PixelFormat::RGBInteger + #endif + ) + return calculateImageDelta<3, T>(actual, expected, output); + else if(expected.format() == PixelFormat::RGBA + #ifndef MAGNUM_TARGET_GLES2 + || expected.format() == PixelFormat::RGBAInteger + #endif + ) + return calculateImageDelta<4, T>(actual, expected, output); + + CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} + +template Float calculateImageDelta(const ImageView2D& actual, const ImageView2D& expected, std::vector& output) { + if( + #if !(defined(MAGNUM_TARGET_WEBGL) && defined(MAGNUM_TARGET_GLES2)) + expected.format() == PixelFormat::Red + #endif + #ifdef MAGNUM_TARGET_GLES2 + #ifndef MAGNUM_TARGET_WEBGL + || + #endif + expected.format() == PixelFormat::Luminance + #endif + ) + return calculateImageDelta<1, T>(actual, expected, output); + else if( + #if !(defined(MAGNUM_TARGET_WEBGL) && defined(MAGNUM_TARGET_GLES2)) + expected.format() == PixelFormat::RG + #endif + #ifdef MAGNUM_TARGET_GLES2 + #ifndef MAGNUM_TARGET_WEBGL + || + #endif + expected.format() == PixelFormat::LuminanceAlpha + #endif + ) + return calculateImageDelta<2, T>(actual, expected, output); + else if(expected.format() == PixelFormat::RGB) + return calculateImageDelta<3, T>(actual, expected, output); + else if(expected.format() == PixelFormat::RGBA) + return calculateImageDelta<4, T>(actual, expected, output); + + CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} + +} + +std::tuple, Float, Float> calculateImageDelta(const ImageView2D& actual, const ImageView2D& expected) { + /* Calculate a delta image */ + std::vector delta(expected.size().product()); + + Float max; + if(expected.type() == PixelType::UnsignedByte) + max = calculateIntegerImageDelta(actual, expected, delta); + else if(expected.type() == PixelType::UnsignedShort) + max = calculateIntegerImageDelta(actual, expected, delta); + else if(expected.type() == PixelType::UnsignedInt) + max = calculateIntegerImageDelta(actual, expected, delta); + #ifndef MAGNUM_TARGET_GLES2 + else if(expected.type() == PixelType::Byte) + max = calculateIntegerImageDelta(actual, expected, delta); + else if(expected.type() == PixelType::Short) + max = calculateIntegerImageDelta(actual, expected, delta); + else if(expected.type() == PixelType::Int) + max = calculateIntegerImageDelta(actual, expected, delta); + #endif + else if(expected.type() == PixelType::Float) + max = calculateImageDelta(actual, expected, delta); + else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ + + /* Calculate mean delta. Do it the special way so we don't lose + precision -- that would result in having false negatives! */ + const Float mean = Math::Algorithms::kahanSum(delta.begin(), delta.end())/delta.size(); + + return std::make_tuple(delta, max, mean); +} + +namespace { + /* Done by printing an white to black gradient using one of the online + ASCII converters. Yes, I'm lazy. Another one could be " .,:;ox%#@". */ + const char Characters[] = " .,:~=+?7IZ$08DNM"; +} + +void printDeltaImage(Debug& out, const std::vector& deltas, const Vector2i& size, const Float max, const Float maxThreshold, const Float meanThreshold) { + CORRADE_INTERNAL_ASSERT(meanThreshold <= maxThreshold); + + /* At most 64 characters per line. The console fonts height is usually 2x + the width, so there is twice the pixels per block */ + const Vector2i pixelsPerBlock{(size.x() + 63)/64, 2*((size.x() + 63)/64)}; + const Vector2i blockCount = (size + pixelsPerBlock - Vector2i{1})/pixelsPerBlock; + + for(std::int_fast32_t y = 0; y != blockCount.y(); ++y) { + out << " |"; + + for(std::int_fast32_t x = 0; x != blockCount.x(); ++x) { + /* Going bottom-up so we don't flip the image upside down when printing */ + const Vector2i offset = Vector2i{Int(x), blockCount.y() - Int(y) - 1}*pixelsPerBlock; + const Vector2i blockSize = Math::min(size - offset, Vector2i{pixelsPerBlock}); + + Float blockMax{}; + for(std::int_fast32_t yb = 0; yb != blockSize.y(); ++yb) + for(std::int_fast32_t xb = 0; xb != blockSize.x(); ++xb) + blockMax = Math::max(blockMax, deltas[(offset.y() + yb)*size.x() + offset.x() + xb]); + + const char c = Characters[Int(Math::round(Math::min(blockMax/max, 1.0f)*(sizeof(Characters) - 2)))]; + + if(blockMax > maxThreshold) + out << Debug::boldColor(Debug::Color::Red) << Debug::nospace << std::string{c} << Debug::resetColor; + else if(blockMax > meanThreshold) + out << Debug::boldColor(Debug::Color::Yellow) << Debug::nospace << std::string{c} << Debug::resetColor; + else out << Debug::nospace << std::string{c}; + } + + out << Debug::nospace << "|" << Debug::newline; + } +} + +namespace { + +template void printIntegerPixelAt(Debug& out, const char* const pixels, const std::size_t stride, const Vector2i& pos, const PixelFormat format) { + if( + #if !(defined(MAGNUM_TARGET_WEBGL) && defined(MAGNUM_TARGET_GLES2)) + format == PixelFormat::Red + #endif + #ifndef MAGNUM_TARGET_GLES2 + || format == PixelFormat::RedInteger + #else + #ifndef MAGNUM_TARGET_WEBGL + || + #endif + format == PixelFormat::Luminance + #endif + ) + out << pixelAt<1, T>(pixels, stride, pos); + else if( + #if !(defined(MAGNUM_TARGET_WEBGL) && defined(MAGNUM_TARGET_GLES2)) + format == PixelFormat::RG + #endif + #ifndef MAGNUM_TARGET_GLES2 + || format == PixelFormat::RGInteger + #else + #ifndef MAGNUM_TARGET_WEBGL + || + #endif + format == PixelFormat::LuminanceAlpha + #endif + ) + out << pixelAt<2, T>(pixels, stride, pos); + /* Take the opportunity and print 8-bit colors in hex */ + else if(format == PixelFormat::RGB + #ifndef MAGNUM_TARGET_GLES2 + || format == PixelFormat::RGBInteger + #endif + ) + out << Math::Color3{pixelAt<3, T>(pixels, stride, pos)}; + else if(format == PixelFormat::RGBA + #ifndef MAGNUM_TARGET_GLES2 + || format == PixelFormat::RGBAInteger + #endif + ) + out << Math::Color4{pixelAt<4, T>(pixels, stride, pos)}; + else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} + +template void printPixelAt(Debug& out, const char* const pixels, const std::size_t stride, const Vector2i& pos, const PixelFormat format) { + if( + #if !(defined(MAGNUM_TARGET_WEBGL) && defined(MAGNUM_TARGET_GLES2)) + format == PixelFormat::Red + #endif + #ifdef MAGNUM_TARGET_GLES2 + #ifndef MAGNUM_TARGET_WEBGL + || + #endif + format == PixelFormat::Luminance + #endif + ) + out << pixelAt<1, T>(pixels, stride, pos); + else if( + #if !(defined(MAGNUM_TARGET_WEBGL) && defined(MAGNUM_TARGET_GLES2)) + format == PixelFormat::RG + #endif + #ifdef MAGNUM_TARGET_GLES2 + #ifndef MAGNUM_TARGET_WEBGL + || + #endif + format == PixelFormat::LuminanceAlpha + #endif + ) + out << pixelAt<2, T>(pixels, stride, pos); + else if(format == PixelFormat::RGB) + out << pixelAt<3, T>(pixels, stride, pos); + else if(format == PixelFormat::RGBA) + out << pixelAt<4, T>(pixels, stride, pos); + else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} + +void printPixelAt(Debug& out, const char* const pixels, const std::size_t stride, const Vector2i& pos, const PixelFormat format, const PixelType type) { + if(type == PixelType::UnsignedByte) + printIntegerPixelAt(out, pixels, stride, pos, format); + else if(type == PixelType::UnsignedShort) + printIntegerPixelAt(out, pixels, stride, pos, format); + else if(type == PixelType::UnsignedInt) + printIntegerPixelAt(out, pixels, stride, pos, format); + #ifndef MAGNUM_TARGET_GLES2 + else if(type == PixelType::Byte) + printIntegerPixelAt(out, pixels, stride, pos, format); + else if(type == PixelType::Short) + printIntegerPixelAt(out, pixels, stride, pos, format); + else if(type == PixelType::Int) + printIntegerPixelAt(out, pixels, stride, pos, format); + #endif + else if(type == PixelType::Float) + printPixelAt(out, pixels, stride, pos, format); + else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ +} + +} + +void printPixelDeltas(Debug& out, const std::vector& delta, const ImageView2D& actual, const ImageView2D& expected, const Float maxThreshold, const Float meanThreshold, std::size_t maxCount) { + /* Precalculate parameters for pixel access */ + Math::Vector2 offset, size; + + std::tie(offset, size, std::ignore) = actual.dataProperties(); + const char* const actualPixels = actual.data() + offset.sum(); + const std::size_t actualStride = size.x(); + + std::tie(offset, size, std::ignore) = expected.dataProperties(); + const char* const expectedPixels = expected.data() + offset.sum(); + const std::size_t expectedStride = size.x(); + + /* Find first maxCount values above mean threshold and put them into a + sorted map */ + std::multimap large; + for(std::size_t i = 0; i != delta.size(); ++i) { + /* GCC 4.7 std::multimap doesn't have emplace() */ + if(delta[i] > meanThreshold) large.insert({delta[i], i}); + } + + CORRADE_INTERNAL_ASSERT(!large.empty()); + + if(large.size() > maxCount) + out << " Top" << maxCount << "out of" << large.size() << "pixels above max/mean threshold:"; + else + out << " Pixels above max/mean threshold:"; + + /* Print the values from largest to smallest. Branching on the done in the + inner loop but that doesn't matter as we always print just ~10 values. */ + std::size_t count = 0; + for(auto it = large.crbegin(); it != large.crend(); ++it) { + if(++count > maxCount) break; + + Vector2i pos; + std::tie(pos.y(), pos.x()) = Math::div(Int(it->second), expected.size().x()); + out << Debug::newline << " [" << Debug::nospace << pos.x() + << Debug::nospace << "," << Debug::nospace << pos.y() + << Debug::nospace << "]"; + + printPixelAt(out, actualPixels, actualStride, pos, expected.format(), expected.type()); + + out << Debug::nospace << ", expected"; + + printPixelAt(out, expectedPixels, expectedStride, pos, expected.format(), expected.type()); + + out << "(Δ =" << Debug::boldColor(delta[it->second] > maxThreshold ? + Debug::Color::Red : Debug::Color::Yellow) << delta[it->second] + << Debug::nospace << Debug::resetColor << ")"; + } +} + +}}} + +#ifndef DOXYGEN_GENERATING_OUTPUT +/* If Doxygen sees this, all @ref Corrade::TestSuite links break (prolly + because the namespace is undocumented in this project) */ +namespace Corrade { namespace TestSuite { + +using namespace Magnum; + +Comparator::Comparator(Float maxThreshold, Float meanThreshold): _maxThreshold{maxThreshold}, _meanThreshold{meanThreshold} { + CORRADE_ASSERT(meanThreshold <= maxThreshold, + "DebugTools::CompareImage: maxThreshold can't be smaller than meanThreshold", ); +} + +bool Comparator::operator()(const ImageView2D& actual, const ImageView2D& expected) { + _actualImage = &actual; + _expectedImage = &expected; + + /* Verify that the images are the same */ + if(actual.size() != expected.size()) { + _state = State::DifferentSize; + return false; + } + if(actual.format() != expected.format() || actual.type() != expected.type()) { + _state = State::DifferentFormat; + return false; + } + + /* Assert on unsupported format/storage */ + #ifndef CORRADE_NO_DEBUG + const bool formatSupported = ( + ( + #if !(defined(MAGNUM_TARGET_WEBGL) && defined(MAGNUM_TARGET_GLES2)) + expected.format() == PixelFormat::Red || + expected.format() == PixelFormat::RG || + #endif + #ifndef MAGNUM_TARGET_GLES2 + expected.format() == PixelFormat::RedInteger || + expected.format() == PixelFormat::RGInteger || + expected.format() == PixelFormat::RGBInteger || + expected.format() == PixelFormat::RGBAInteger || + #else + expected.format() == PixelFormat::Luminance || + expected.format() == PixelFormat::LuminanceAlpha || + #endif + expected.format() == PixelFormat::RGB || + expected.format() == PixelFormat::RGBA + ) && ( + #ifndef MAGNUM_TARGET_GLES2 + expected.type() == PixelType::Byte || + expected.type() == PixelType::Short || + expected.type() == PixelType::Int || + #endif + expected.type() == PixelType::UnsignedByte || + expected.type() == PixelType::UnsignedShort || + expected.type() == PixelType::UnsignedInt + )) || (( + #if !(defined(MAGNUM_TARGET_WEBGL) && defined(MAGNUM_TARGET_GLES2)) + expected.format() == PixelFormat::Red || + expected.format() == PixelFormat::RG || + #endif + #ifdef MAGNUM_TARGET_GLES2 + expected.format() == PixelFormat::Luminance || + expected.format() == PixelFormat::LuminanceAlpha || + #endif + expected.format() == PixelFormat::RGB || + expected.format() == PixelFormat::RGBA + ) && expected.type() == PixelType::Float); + CORRADE_ASSERT( + formatSupported, + "DebugTools::CompareImage: format" << expected.format() << Debug::nospace << "/" << expected.type() << "is not supported", {}); + #endif + #ifndef MAGNUM_TARGET_GLES + CORRADE_ASSERT(!actual.storage().swapBytes() && !expected.storage().swapBytes(), + "DebugTools::CompareImage: pixel storage with byte swap is not supported", {}); + #endif + + std::vector delta; + std::tie(delta, _max, _mean) = DebugTools::Implementation::calculateImageDelta(actual, expected); + + /* If both values are not above threshold, success */ + if(_max > _maxThreshold && _mean > _meanThreshold) + _state = State::AboveThresholds; + else if(_max > _maxThreshold) + _state = State::AboveMaxThreshold; + else if(_mean > _meanThreshold) + _state = State::AboveMeanThreshold; + else return true; + + /* Otherwise save the deltas and fail */ + _delta = std::move(delta); + return false; +} + +void Comparator::printErrorMessage(Debug& out, const std::string& actual, const std::string& expected) const { + out << "Images" << actual << "and" << expected << "have"; + if(_state == State::DifferentSize) + out << "different size, actual" << _actualImage->size() << "but" + << _expectedImage->size() << "expected."; + else if(_state == State::DifferentFormat) + out << "different format, actual" << _actualImage->format() + << Debug::nospace << "/" << Debug::nospace << _actualImage->type() + << "but" << _expectedImage->format() << Debug::nospace << "/" + << Debug::nospace << _expectedImage->type() << "expected."; + else { + if(_state == State::AboveThresholds) + out << "both max and mean delta above threshold, actual" + << _max << Debug::nospace << "/" << Debug::nospace << _mean + << "but at most" << _maxThreshold << Debug::nospace << "/" + << Debug::nospace << _meanThreshold << "expected."; + else if(_state == State::AboveMaxThreshold) + out << "max delta above threshold, actual" << _max + << "but at most" << _maxThreshold + << "expected. Mean delta" << _mean << "is below threshold" + << _meanThreshold << Debug::nospace << "."; + else if(_state == State::AboveMeanThreshold) + out << "mean delta above threshold, actual" << _mean + << "but at most" << _meanThreshold + << "expected. Max delta" << _max << "is below threshold" + << _maxThreshold << Debug::nospace << "."; + else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ + + out << "Delta image:" << Debug::newline; + DebugTools::Implementation::printDeltaImage(out, _delta, _expectedImage->size(), _max, _maxThreshold, _meanThreshold); + DebugTools::Implementation::printPixelDeltas(out, _delta, *_actualImage, *_expectedImage, _maxThreshold, _meanThreshold, 10); + } +} + +}} +#endif diff --git a/src/Magnum/DebugTools/CompareImage.h b/src/Magnum/DebugTools/CompareImage.h new file mode 100644 index 000000000..1da459136 --- /dev/null +++ b/src/Magnum/DebugTools/CompareImage.h @@ -0,0 +1,173 @@ +#ifndef Magnum_DebugTools_CompareImage_h +#define Magnum_DebugTools_CompareImage_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016 + Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +/** @file + * @brief Class @ref Magnum::DebugTools::CompareImage + */ + +#include +#include + +#include "Magnum/Magnum.h" +#include "Magnum/Math/Vector2.h" +#include "Magnum/DebugTools/visibility.h" + +namespace Magnum { namespace DebugTools { + +namespace Implementation { + MAGNUM_DEBUGTOOLS_EXPORT std::tuple, Float, Float> calculateImageDelta(const ImageView2D& actual, const ImageView2D& expected); + + MAGNUM_DEBUGTOOLS_EXPORT void printDeltaImage(Debug& out, const std::vector& delta, const Vector2i& size, Float max, Float maxThreshold, Float meanThreshold); + + MAGNUM_DEBUGTOOLS_EXPORT void printPixelDeltas(Debug& out, const std::vector& delta, const ImageView2D& actual, const ImageView2D& expected, Float maxThreshold, Float meanThreshold, std::size_t maxCount); +} + +class CompareImage; + +}} + +#ifndef DOXYGEN_GENERATING_OUTPUT +/* If Doxygen sees this, all @ref Corrade::TestSuite links break (prolly + because the namespace is undocumented in this project) */ +namespace Corrade { namespace TestSuite { + +template<> class MAGNUM_DEBUGTOOLS_EXPORT Comparator { + public: + explicit Comparator(Magnum::Float maxThreshold, Magnum::Float meanThreshold); + + /*implicit*/ Comparator(): Comparator{0.0f, 0.0f} {} + + bool operator()(const Magnum::ImageView2D& actual, const Magnum::ImageView2D& expected); + + void printErrorMessage(Utility::Debug& out, const std::string& actual, const std::string& expected) const; + + private: + enum class State { + DifferentSize = 1, + DifferentFormat, + AboveThresholds, + AboveMeanThreshold, + AboveMaxThreshold + }; + + Magnum::Float _maxThreshold, _meanThreshold; + + State _state{}; + const Magnum::ImageView2D *_actualImage, *_expectedImage; + Magnum::Float _max, _mean; + std::vector _delta; +}; + +}} +#endif + +namespace Magnum { namespace DebugTools { + +/** +@brief Image comparator + +To be used with @ref Corrade::TestSuite. Basic use is really simple: + +@snippet debugtools-compareimage.cpp 0 + +Based on actual images used, in case of commparison failure the comparator can +give for example the following result: + +@image html debugtools-compareimage.png + +Supports the following formats: + +- @ref PixelFormat::Red, @ref PixelFormat::RedInteger, @ref PixelFormat::RG, + @ref PixelFormat::RGInteger, @ref PixelFormat::RGB, @ref PixelFormat::RGBInteger, + @ref PixelFormat::RGBA and @ref PixelFormat::RGBAInteger with + @ref PixelType::UnsignedByte, @ref PixelType::Byte, @ref PixelType::UnsignedShort, + @ref PixelType::Short, @ref PixelType::UnsignedInt and @ref PixelType::Int +- @ref PixelFormat::Red, @ref PixelFormat::RG, @ref PixelFormat::RGB and + @ref PixelFormat::RGBA with @ref PixelType::Float + +In OpenGL ES 2.0 and WebGL 1.0, @ref PixelFormat::Luminance and +@ref PixelFormat::LuminanceAlpha are also accepted in place of +@ref PixelFormat::Red and @ref PixelFormat::RG. + +Supports all @ref PixelStorage parameters *except* non-default +@ref PixelStorage::swapBytes() values. The images don't need to have the same +pixel storage parameters, meaning you are able to compare different subimages +of a larger image as long as they have the same size. + +The comparator first compares both images to have the same pixel format/type +combination and size. Each pixel is then first converted to @ref Magnum::Float "Float" +vector of corresponding channel count and then the per-pixel delta is +calculated as simple sum of per-channel deltas (where @f$ \boldsymbol{a} @f$ is +the actual pixel value, @f$ \boldsymbol{e} @f$ expected pixel value and @f$ c @f$ +is channel count), with max and mean delta being taken over the whole picture. @f[ + + \Delta_{\boldsymbol{p}} = \sum\limits_{i=1}^c \dfrac{a_i - e_i}{c} + +@f] + +The two parameters passed to the @ref CompareImage(Float, Float) "CompareImage(Float, Float)" +constructor are max and mean delta threshold. If the calculated values are +above these threshold, the comparison fails. In case of comparison failure the +diagnostic output contains calculated max/meanvalues, delta image visualization +and a list of top deltas. The delta image is an ASCII-art representation of the +image difference with each block being a maximum of pixel deltas in some area, +printed as characters of different perceived brightness. Blocks with delta over +the max threshold are colored red, blocks with delta over the mean threshold +are colored yellow. The delta list contains X,Y pixel position (with origin at +bottom left), actual and expected pixel value and calculated delta. +*/ +class CompareImage { + public: + /** + * @brief Constructor + * @param maxThreshold Max threshold. If any pixel has delta above + * this value, this comparison fails + * @param meanThreshold Mean threshold. If mean delta over all pixels + * is above this value, the comparison fails + */ + explicit CompareImage(Float maxThreshold, Float meanThreshold): _c{maxThreshold, meanThreshold} {} + + /** + * @brief Implicit constructor + * + * Equivalent to calling the above with zero values. + */ + explicit CompareImage(): CompareImage{0.0f, 0.0f} {} + + #ifndef DOXYGEN_GENERATING_OUTPUT + Corrade::TestSuite::Comparator& comparator() { + return _c; + } + #endif + + private: + Corrade::TestSuite::Comparator _c; +}; + +}} + +#endif diff --git a/src/Magnum/DebugTools/Test/CMakeLists.txt b/src/Magnum/DebugTools/Test/CMakeLists.txt index 1610b5a50..b200d2b95 100644 --- a/src/Magnum/DebugTools/Test/CMakeLists.txt +++ b/src/Magnum/DebugTools/Test/CMakeLists.txt @@ -33,6 +33,10 @@ if(WITH_SCENEGRAPH) corrade_add_test(DebugToolsForceRendererTest ForceRendererTest.cpp LIBRARIES MagnumMathTestLib) endif() +if(Corrade_TestSuite_FOUND) + corrade_add_test(DebugToolsCompareImageTest CompareImageTest.cpp LIBRARIES MagnumDebugTools) +endif() + if(BUILD_GL_TESTS) corrade_add_test(DebugToolsBufferDataGLTest BufferDataGLTest.cpp LIBRARIES MagnumDebugTools ${GL_TEST_LIBRARIES}) corrade_add_test(DebugToolsTextureImageGLTest TextureImageGLTest.cpp LIBRARIES MagnumDebugTools ${GL_TEST_LIBRARIES}) diff --git a/src/Magnum/DebugTools/Test/CompareImageTest.cpp b/src/Magnum/DebugTools/Test/CompareImageTest.cpp new file mode 100644 index 000000000..f6997d904 --- /dev/null +++ b/src/Magnum/DebugTools/Test/CompareImageTest.cpp @@ -0,0 +1,396 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016 + Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include +#include + +#include "Magnum/ImageView.h" +#include "Magnum/PixelFormat.h" +#include "Magnum/DebugTools/CompareImage.h" +#include "Magnum/Math/Functions.h" +#include "Magnum/Math/Color.h" + +namespace Magnum { namespace DebugTools { namespace Test { + +struct CompareImageTest: TestSuite::Tester { + explicit CompareImageTest(); + + void calculateDelta(); + void calculateDeltaStorage(); + + void deltaImage(); + void deltaImageScaling(); + void deltaImageColors(); + + void pixelDelta(); + void pixelDeltaOverflow(); + + void compareDifferentSize(); + void compareDifferentFormat(); + void compareDifferentType(); + void compareSameZeroThreshold(); + void compareAboveThresholds(); + void compareAboveMaxThreshold(); + void compareAboveMeanThreshold(); +}; + +CompareImageTest::CompareImageTest() { + addTests({&CompareImageTest::calculateDelta, + &CompareImageTest::calculateDeltaStorage, + + &CompareImageTest::deltaImage, + &CompareImageTest::deltaImageScaling, + &CompareImageTest::deltaImageColors, + + &CompareImageTest::pixelDelta, + &CompareImageTest::pixelDeltaOverflow, + + &CompareImageTest::compareDifferentSize, + &CompareImageTest::compareDifferentFormat, + &CompareImageTest::compareDifferentType, + &CompareImageTest::compareSameZeroThreshold, + &CompareImageTest::compareAboveThresholds, + &CompareImageTest::compareAboveMaxThreshold, + &CompareImageTest::compareAboveMeanThreshold}); +} + +namespace { + const Float ActualRedData[] = { + 0.3f, 1.0f, 0.9f, + 0.9f, 0.6f, 0.2f, + -0.1f, 1.0f, 0.0f + }; + + const Float ExpectedRedData[] = { + 0.65f, 1.0f, 0.6f, + 0.91f, 0.6f, 0.1f, + 0.02f, 0.0f, 0.0f + }; + + const std::vector DeltaRed{ + 0.35f, 0.0f, 0.3f, + 0.01f, 0.0f, 0.1f, + 0.12f, 1.0f, 0.0f}; + + const ImageView2D ActualRed{ + #if !(defined(MAGNUM_TARGET_WEBGL) && defined(MAGNUM_TARGET_GLES2)) + PixelFormat::Red + #else + PixelFormat::Luminance + #endif + , PixelType::Float, {3, 3}, ActualRedData}; + const ImageView2D ExpectedRed{ + #if !(defined(MAGNUM_TARGET_WEBGL) && defined(MAGNUM_TARGET_GLES2)) + PixelFormat::Red + #else + PixelFormat::Luminance + #endif + , PixelType::Float, {3, 3}, ExpectedRedData}; +} + +void CompareImageTest::calculateDelta() { + std::vector delta; + Float max, mean; + std::tie(delta, max, mean) = Implementation::calculateImageDelta(ActualRed, ExpectedRed); + + CORRADE_COMPARE_AS(delta, DeltaRed, TestSuite::Compare::Container); + CORRADE_COMPARE(max, 1.0f); + CORRADE_COMPARE(mean, 0.208889f); +} + +namespace { + /* Different storage for each */ + const UnsignedByte ActualRgbData[] = { + 0, 0, 0, 0, 0, 0, 0, 0, + 0x56, 0xf8, 0x3a, 0x56, 0x47, 0xec, 0, 0, + 0x23, 0x57, 0x10, 0xab, 0xcd, 0x85, 0, 0 + }; + + const UnsignedByte ExpectedRgbData[] = { + #if !(defined(MAGNUM_TARGET_WEBGL) && defined(MAGNUM_TARGET_GLES2)) + 0, 0, 0, 0x55, 0xf8, 0x3a, 0x56, 0x10, 0xed, 0, 0, 0, + 0, 0, 0, 0x23, 0x27, 0x10, 0xab, 0xcd, 0xfa, 0, 0, 0 + #else + 0x55, 0xf8, 0x3a, 0x56, 0x10, 0xed, 0, 0, + 0x23, 0x27, 0x10, 0xab, 0xcd, 0xfa, 0, 0, + #endif + }; + + const ImageView2D ActualRgb{PixelStorage{}.setSkip({0, 1, 0}), + PixelFormat::RGB, PixelType::UnsignedByte, {2, 2}, ActualRgbData}; + const ImageView2D ExpectedRgb{ + #if !(defined(MAGNUM_TARGET_WEBGL) && defined(MAGNUM_TARGET_GLES2)) + PixelStorage{}.setSkip({1, 0, 0}).setRowLength(3), + #endif + PixelFormat::RGB, PixelType::UnsignedByte, {2, 2}, ExpectedRgbData}; +} + +void CompareImageTest::calculateDeltaStorage() { + std::vector delta; + Float max, mean; + std::tie(delta, max, mean) = Implementation::calculateImageDelta(ActualRgb, ExpectedRgb); + + CORRADE_COMPARE_AS(delta, (std::vector{ + 1.0f/3.0f, (55.0f + 1.0f)/3.0f, + 48.0f/3.0f, 117.0f/3.0f + }), TestSuite::Compare::Container); + CORRADE_COMPARE(max, 117.0f/3.0f); + CORRADE_COMPARE(mean, 18.5f); +} + +void CompareImageTest::deltaImage() { + std::ostringstream out; + Debug d{&out, Debug::Flag::DisableColors}; + + std::vector delta(32*32); + + for(std::int_fast32_t x = 0; x != 32; ++x) + for(std::int_fast32_t y = 0; y != 32; ++y) + delta[y*32 + x] = Vector2{Float(x), Float(y)}.length()/Vector2{32.0f}.length(); + + Implementation::printDeltaImage(d, delta, {32, 32}, 1.0f, 0.0f, 0.0f); + CORRADE_COMPARE(out.str(), + " |$$$$$$$$$$0000000888888DDDDNNNNM|\n" + " |ZZZZZZZ$$$$$$$$0000008888DDDDNNN|\n" + " |ZZZZZZZZZZZZZ$$$$$$00008888DDDDN|\n" + " |IIIIIIIIIIZZZZZZZ$$$$00008888DDD|\n" + " |7777777IIIIIIIZZZZZ$$$$00008888D|\n" + " |???777777777IIIIIZZZZ$$$$0000888|\n" + " |??????????77777IIIIZZZZ$$$$00088|\n" + " |+++++++??????7777IIIIZZZZ$$$0008|\n" + " |=====++++++????7777IIIIZZZ$$$000|\n" + " |=========++++????7777IIIZZZ$$$00|\n" + " |~~~~~~~====++++????777IIIZZZ$$$0|\n" + " |:::::~~~~====++++???777IIIZZZ$$$|\n" + " |,::::::~~~~===+++????77IIIZZZ$$$|\n" + " |,,,,,::::~~~===+++???777IIIZZZ$$|\n" + " |...,,,,:::~~~===+++??777IIIZZZ$$|\n" + " | ....,,:::~~~===+++???777IIZZZ$$|\n"); +} + +void CompareImageTest::deltaImageScaling() { + std::ostringstream out; + Debug d{&out, Debug::Flag::DisableColors}; + + std::vector delta(65*40); + for(std::int_fast32_t x = 0; x != 65; ++x) + for(std::int_fast32_t y = 0; y != 40; ++y) + delta[y*65 + x] = Vector2{Float(x), Float(y)}.length()/Vector2{65.0f, 40.0f}.length(); + + Implementation::printDeltaImage(d, delta, {65, 40}, 1.0f, 0.0f, 0.0f); + CORRADE_COMPARE(out.str(), + " |777777IIIIIIZZZZ$$$0000888DDDNNMM|\n" + " |????777777IIIIZZZZ$$$000888DDDNNN|\n" + " |?????????7777IIIIZZZ$$$00888DDDNN|\n" + " |++++++++????777IIIZZZ$$$00088DDDN|\n" + " |======++++????777IIIZZ$$$00088DDD|\n" + " |~~~~~====+++???777IIIZZ$$$00888DD|\n" + " |::::~~~~===+++??777IIZZZ$$00088DD|\n" + " |,,::::~~~===++???777IIZZ$$$00888D|\n" + " |.,,,,:::~~===++???77IIZZZ$$000888|\n" + " |...,,,::~~~==++???77IIIZZ$$000888|\n"); +} + +void CompareImageTest::deltaImageColors() { + /* Print for visual color verification */ + { + Debug() << "Visual verification -- some letters should be yellow, some red, some white:"; + Debug d{Debug::Flag::NoNewlineAtTheEnd}; + Implementation::printDeltaImage(d, DeltaRed, {3, 3}, 2.0f, 0.5f, 0.2f); + } + + std::ostringstream out; + Debug dc{&out, Debug::Flag::DisableColors}; + Implementation::printDeltaImage(dc, DeltaRed, {3, 3}, 2.0f, 0.5f, 0.2f); + /* Yes, there is half of the rows (2 instead of 3) in order to roughly + preserve image ratio */ + CORRADE_COMPARE(out.str(), + " |.7 |\n" + " |: ,|\n"); +} + +void CompareImageTest::pixelDelta() { + { + Debug() << "Visual verification -- some lines should be yellow, some red:"; + Debug d; + Implementation::printPixelDeltas(d, DeltaRed, ActualRed, ExpectedRed, 0.5f, 0.1f, 10); + } + + std::ostringstream out; + Debug d{&out, Debug::Flag::DisableColors}; + Implementation::printPixelDeltas(d, DeltaRed, ActualRed, ExpectedRed, 0.5f, 0.1f, 10); + + CORRADE_COMPARE(out.str(), R"( Pixels above max/mean threshold: + [1,2] Vector(1), expected Vector(0) (Δ = 1) + [0,0] Vector(0.3), expected Vector(0.65) (Δ = 0.35) + [2,0] Vector(0.9), expected Vector(0.6) (Δ = 0.3) + [0,2] Vector(-0.1), expected Vector(0.02) (Δ = 0.12))"); +} + +void CompareImageTest::pixelDeltaOverflow() { + std::ostringstream out; + Debug d{&out, Debug::Flag::DisableColors}; + Implementation::printPixelDeltas(d, DeltaRed, ActualRed, ExpectedRed, 0.5f, 0.1f, 3); + + CORRADE_COMPARE(out.str(), R"( Top 3 out of 4 pixels above max/mean threshold: + [1,2] Vector(1), expected Vector(0) (Δ = 1) + [0,0] Vector(0.3), expected Vector(0.65) (Δ = 0.35) + [2,0] Vector(0.9), expected Vector(0.6) (Δ = 0.3))"); +} + +void CompareImageTest::compareDifferentSize() { + std::stringstream out; + + ImageView2D a{ + #ifndef MAGNUM_TARGET_GLES2 + PixelFormat::RGInteger, + #else + PixelFormat::LuminanceAlpha, + #endif + PixelType::UnsignedByte, {3, 4}, nullptr}; + ImageView2D b{ + #ifndef MAGNUM_TARGET_GLES2 + PixelFormat::RGInteger, + #else + PixelFormat::LuminanceAlpha, + #endif + PixelType::UnsignedByte, {3, 5}, nullptr}; + + { + Error e(&out); + TestSuite::Comparator compare; + CORRADE_VERIFY(!compare(a, b)); + compare.printErrorMessage(e, "a", "b"); + } + + CORRADE_COMPARE(out.str(), "Images a and b have different size, actual Vector(3, 4) but Vector(3, 5) expected.\n"); +} + +void CompareImageTest::compareDifferentFormat() { + std::stringstream out; + + ImageView2D a{PixelFormat::RGBA, PixelType::Float, {3, 4}, nullptr}; + ImageView2D b{PixelFormat::RGB, PixelType::Float, {3, 4}, nullptr}; + + { + Error e(&out); + TestSuite::Comparator compare; + CORRADE_VERIFY(!compare(a, b)); + compare.printErrorMessage(e, "a", "b"); + } + + CORRADE_COMPARE(out.str(), "Images a and b have different format, actual PixelFormat::RGBA/PixelType::Float but PixelFormat::RGB/PixelType::Float expected.\n"); +} + +void CompareImageTest::compareDifferentType() { + std::stringstream out; + + ImageView2D a{PixelFormat::RGB, PixelType::UnsignedByte, {3, 4}, nullptr}; + ImageView2D b{PixelFormat::RGB, PixelType::UnsignedShort, {3, 4}, nullptr}; + + { + Error e(&out); + TestSuite::Comparator compare; + CORRADE_VERIFY(!compare(a, b)); + compare.printErrorMessage(e, "a", "b"); + } + + CORRADE_COMPARE(out.str(), "Images a and b have different format, actual PixelFormat::RGB/PixelType::UnsignedByte but PixelFormat::RGB/PixelType::UnsignedShort expected.\n"); +} + +void CompareImageTest::compareSameZeroThreshold() { + using namespace Math::Literals; + + const Color3 data[] = { + 0xcafeba_rgbf, 0xdeadbe_rgbf, + 0xbadc0d_rgbf, 0xbeefe0_rgbf + }; + + const ImageView2D image{PixelFormat::RGB, PixelType::Float, {2, 2}, data}; + CORRADE_VERIFY((TestSuite::Comparator{0.0f, 0.0f}(image, image))); +} + +void CompareImageTest::compareAboveThresholds() { + std::stringstream out; + + { + TestSuite::Comparator compare{20.0f, 10.0f}; + CORRADE_VERIFY(!compare(ActualRgb, ExpectedRgb)); + Debug d{&out, Debug::Flag::DisableColors}; + compare.printErrorMessage(d, "a", "b"); + } + + CORRADE_COMPARE(out.str(), +R"(Images a and b have both max and mean delta above threshold, actual 39/18.5 but at most 20/10 expected. Delta image: + |?M| + Pixels above max/mean threshold: + [1,1] #abcd85, expected #abcdfa (Δ = 39) + [1,0] #5647ec, expected #5610ed (Δ = 18.6667) + [0,1] #235710, expected #232710 (Δ = 16) +)"); +} + +void CompareImageTest::compareAboveMaxThreshold() { + std::stringstream out; + + { + TestSuite::Comparator compare{30.0f, 20.0f}; + CORRADE_VERIFY(!compare(ActualRgb, ExpectedRgb)); + Debug d{&out, Debug::Flag::DisableColors}; + compare.printErrorMessage(d, "a", "b"); + } + + CORRADE_COMPARE(out.str(), +R"(Images a and b have max delta above threshold, actual 39 but at most 30 expected. Mean delta 18.5 is below threshold 20. Delta image: + |?M| + Pixels above max/mean threshold: + [1,1] #abcd85, expected #abcdfa (Δ = 39) +)"); +} + +void CompareImageTest::compareAboveMeanThreshold() { + std::stringstream out; + + { + TestSuite::Comparator compare{50.0f, 18.0f}; + CORRADE_VERIFY(!compare(ActualRgb, ExpectedRgb)); + Debug d{&out, Debug::Flag::DisableColors}; + compare.printErrorMessage(d, "a", "b"); + } + + CORRADE_COMPARE(out.str(), +R"(Images a and b have mean delta above threshold, actual 18.5 but at most 18 expected. Max delta 39 is below threshold 50. Delta image: + |?M| + Pixels above max/mean threshold: + [1,1] #abcd85, expected #abcdfa (Δ = 39) + [1,0] #5647ec, expected #5610ed (Δ = 18.6667) +)"); +} + +}}} + +CORRADE_TEST_MAIN(Magnum::DebugTools::Test::CompareImageTest)