mirror of https://github.com/mosra/magnum.git
Browse Source
Currently just does per-pixel comparison and calculates absolute delta, failing the comparison if max/mean delta threshold is above specified values. Useful enough for the case I have right now, might fail in other case -- but still better than whatever else I was using before :)pull/190/head
13 changed files with 1250 additions and 1 deletions
@ -0,0 +1,40 @@ |
|||||||
|
# |
||||||
|
# This file is part of Magnum. |
||||||
|
# |
||||||
|
# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016 |
||||||
|
# Vladimír Vondruš <mosra@centrum.cz> |
||||||
|
# |
||||||
|
# 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() |
||||||
@ -0,0 +1,27 @@ |
|||||||
|
/* |
||||||
|
This file is part of Magnum. |
||||||
|
|
||||||
|
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016 |
||||||
|
Vladimír Vondruš <mosra@centrum.cz> |
||||||
|
|
||||||
|
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}" |
||||||
@ -0,0 +1,82 @@ |
|||||||
|
/*
|
||||||
|
This file is part of Magnum. |
||||||
|
|
||||||
|
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016 |
||||||
|
Vladimír Vondruš <mosra@centrum.cz> |
||||||
|
|
||||||
|
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 <Corrade/PluginManager/Manager.h> |
||||||
|
#include <Corrade/TestSuite/Tester.h> |
||||||
|
#include <Corrade/Utility/Directory.h> |
||||||
|
|
||||||
|
#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<Trade::AbstractImporter> manager{MAGNUM_PLUGINS_IMPORTER_DIR}; |
||||||
|
std::unique_ptr<Trade::AbstractImporter> 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<Trade::AbstractImporter> manager{MAGNUM_PLUGINS_IMPORTER_DIR}; |
||||||
|
std::unique_ptr<Trade::AbstractImporter> 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) |
||||||
|
|
||||||
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
@ -0,0 +1,507 @@ |
|||||||
|
/*
|
||||||
|
This file is part of Magnum. |
||||||
|
|
||||||
|
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016 |
||||||
|
Vladimír Vondruš <mosra@centrum.cz> |
||||||
|
|
||||||
|
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 <map> |
||||||
|
#include <sstream> |
||||||
|
#include <vector> |
||||||
|
|
||||||
|
#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<std::size_t size, class T> Math::Vector<size, T> pixelAt(const char* const pixels, const std::size_t stride, const Vector2i& pos) { |
||||||
|
return reinterpret_cast<const Math::Vector<size, T>*>(pixels + stride*pos.y())[pos.x()]; |
||||||
|
} |
||||||
|
|
||||||
|
template<std::size_t size, class T> Float calculateImageDelta(const ImageView2D& actual, const ImageView2D& expected, std::vector<Float>& output) { |
||||||
|
CORRADE_INTERNAL_ASSERT(output.size() == std::size_t(expected.size().product())); |
||||||
|
|
||||||
|
/* Precalculate parameters for pixel access */ |
||||||
|
Math::Vector2<std::size_t> 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<size, Float> actualPixel{pixelAt<size, T>(actualPixels, actualStride, {Int(x), Int(y)})}; |
||||||
|
Math::Vector<size, Float> expectedPixel{pixelAt<size, T>(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<class T> Float calculateIntegerImageDelta(const ImageView2D& actual, const ImageView2D& expected, std::vector<Float>& 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<class T> Float calculateImageDelta(const ImageView2D& actual, const ImageView2D& expected, std::vector<Float>& 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<std::vector<Float>, Float, Float> calculateImageDelta(const ImageView2D& actual, const ImageView2D& expected) { |
||||||
|
/* Calculate a delta image */ |
||||||
|
std::vector<Float> delta(expected.size().product()); |
||||||
|
|
||||||
|
Float max; |
||||||
|
if(expected.type() == PixelType::UnsignedByte) |
||||||
|
max = calculateIntegerImageDelta<UnsignedByte>(actual, expected, delta); |
||||||
|
else if(expected.type() == PixelType::UnsignedShort) |
||||||
|
max = calculateIntegerImageDelta<UnsignedShort>(actual, expected, delta); |
||||||
|
else if(expected.type() == PixelType::UnsignedInt) |
||||||
|
max = calculateIntegerImageDelta<UnsignedInt>(actual, expected, delta); |
||||||
|
#ifndef MAGNUM_TARGET_GLES2 |
||||||
|
else if(expected.type() == PixelType::Byte) |
||||||
|
max = calculateIntegerImageDelta<Byte>(actual, expected, delta); |
||||||
|
else if(expected.type() == PixelType::Short) |
||||||
|
max = calculateIntegerImageDelta<Short>(actual, expected, delta); |
||||||
|
else if(expected.type() == PixelType::Int) |
||||||
|
max = calculateIntegerImageDelta<Int>(actual, expected, delta); |
||||||
|
#endif |
||||||
|
else if(expected.type() == PixelType::Float) |
||||||
|
max = calculateImageDelta<Float>(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<Float>& 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<class T> 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<T>{pixelAt<3, T>(pixels, stride, pos)}; |
||||||
|
else if(format == PixelFormat::RGBA |
||||||
|
#ifndef MAGNUM_TARGET_GLES2 |
||||||
|
|| format == PixelFormat::RGBAInteger |
||||||
|
#endif |
||||||
|
) |
||||||
|
out << Math::Color4<T>{pixelAt<4, T>(pixels, stride, pos)}; |
||||||
|
else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ |
||||||
|
} |
||||||
|
|
||||||
|
template<class T> 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<UnsignedByte>(out, pixels, stride, pos, format); |
||||||
|
else if(type == PixelType::UnsignedShort) |
||||||
|
printIntegerPixelAt<UnsignedShort>(out, pixels, stride, pos, format); |
||||||
|
else if(type == PixelType::UnsignedInt) |
||||||
|
printIntegerPixelAt<UnsignedInt>(out, pixels, stride, pos, format); |
||||||
|
#ifndef MAGNUM_TARGET_GLES2 |
||||||
|
else if(type == PixelType::Byte) |
||||||
|
printIntegerPixelAt<Byte>(out, pixels, stride, pos, format); |
||||||
|
else if(type == PixelType::Short) |
||||||
|
printIntegerPixelAt<Short>(out, pixels, stride, pos, format); |
||||||
|
else if(type == PixelType::Int) |
||||||
|
printIntegerPixelAt<Int>(out, pixels, stride, pos, format); |
||||||
|
#endif |
||||||
|
else if(type == PixelType::Float) |
||||||
|
printPixelAt<Float>(out, pixels, stride, pos, format); |
||||||
|
else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
void printPixelDeltas(Debug& out, const std::vector<Float>& delta, const ImageView2D& actual, const ImageView2D& expected, const Float maxThreshold, const Float meanThreshold, std::size_t maxCount) { |
||||||
|
/* Precalculate parameters for pixel access */ |
||||||
|
Math::Vector2<std::size_t> 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<Float, std::size_t> 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<DebugTools::CompareImage>::Comparator(Float maxThreshold, Float meanThreshold): _maxThreshold{maxThreshold}, _meanThreshold{meanThreshold} { |
||||||
|
CORRADE_ASSERT(meanThreshold <= maxThreshold, |
||||||
|
"DebugTools::CompareImage: maxThreshold can't be smaller than meanThreshold", ); |
||||||
|
} |
||||||
|
|
||||||
|
bool Comparator<DebugTools::CompareImage>::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<Float> 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<DebugTools::CompareImage>::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 |
||||||
@ -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š <mosra@centrum.cz> |
||||||
|
|
||||||
|
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 <vector> |
||||||
|
#include <Corrade/TestSuite/Comparator.h> |
||||||
|
|
||||||
|
#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<std::vector<Float>, Float, Float> calculateImageDelta(const ImageView2D& actual, const ImageView2D& expected); |
||||||
|
|
||||||
|
MAGNUM_DEBUGTOOLS_EXPORT void printDeltaImage(Debug& out, const std::vector<Float>& delta, const Vector2i& size, Float max, Float maxThreshold, Float meanThreshold); |
||||||
|
|
||||||
|
MAGNUM_DEBUGTOOLS_EXPORT void printPixelDeltas(Debug& out, const std::vector<Float>& 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<Magnum::DebugTools::CompareImage> { |
||||||
|
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<Magnum::Float> _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<CompareImage>& comparator() { |
||||||
|
return _c; |
||||||
|
} |
||||||
|
#endif |
||||||
|
|
||||||
|
private: |
||||||
|
Corrade::TestSuite::Comparator<CompareImage> _c; |
||||||
|
}; |
||||||
|
|
||||||
|
}} |
||||||
|
|
||||||
|
#endif |
||||||
@ -0,0 +1,396 @@ |
|||||||
|
/*
|
||||||
|
This file is part of Magnum. |
||||||
|
|
||||||
|
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016 |
||||||
|
Vladimír Vondruš <mosra@centrum.cz> |
||||||
|
|
||||||
|
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 <sstream> |
||||||
|
#include <numeric> |
||||||
|
#include <Corrade/TestSuite/Tester.h> |
||||||
|
#include <Corrade/TestSuite/Compare/Container.h> |
||||||
|
|
||||||
|
#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<Float> 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<Float> 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<Float> delta; |
||||||
|
Float max, mean; |
||||||
|
std::tie(delta, max, mean) = Implementation::calculateImageDelta(ActualRgb, ExpectedRgb); |
||||||
|
|
||||||
|
CORRADE_COMPARE_AS(delta, (std::vector<Float>{ |
||||||
|
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<Float> 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<Float> 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<CompareImage> 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<CompareImage> 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<CompareImage> 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<CompareImage>{0.0f, 0.0f}(image, image))); |
||||||
|
} |
||||||
|
|
||||||
|
void CompareImageTest::compareAboveThresholds() { |
||||||
|
std::stringstream out; |
||||||
|
|
||||||
|
{ |
||||||
|
TestSuite::Comparator<CompareImage> 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<CompareImage> 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<CompareImage> 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) |
||||||
Loading…
Reference in new issue