Browse Source

DebugTools: initial implementation of CompareImage class.

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
Vladimír Vondruš 9 years ago
parent
commit
c45472a0f0
  1. 5
      CMakeLists.txt
  2. 4
      Doxyfile
  3. 40
      doc/snippets/CMakeLists.txt
  4. 27
      doc/snippets/configure.h.cmake
  5. 82
      doc/snippets/debugtools-compareimage.cpp
  6. BIN
      doc/snippets/debugtools-compareimage.png
  7. BIN
      doc/snippets/image1.tga
  8. BIN
      doc/snippets/image2.tga
  9. 13
      src/Magnum/DebugTools/CMakeLists.txt
  10. 507
      src/Magnum/DebugTools/CompareImage.cpp
  11. 173
      src/Magnum/DebugTools/CompareImage.h
  12. 4
      src/Magnum/DebugTools/Test/CMakeLists.txt
  13. 396
      src/Magnum/DebugTools/Test/CompareImageTest.cpp

5
CMakeLists.txt

@ -335,3 +335,8 @@ set(MAGNUM_PLUGINS_AUDIOIMPORTER_RELEASE_DIR ${MAGNUM_PLUGINS_RELEASE_DIR}/audio
add_subdirectory(modules) add_subdirectory(modules)
add_subdirectory(src) add_subdirectory(src)
# Build snippets as part of testing
if(BUILD_TESTS)
add_subdirectory(doc/snippets)
endif()

4
Doxyfile

@ -892,7 +892,8 @@ EXCLUDE_SYMBOLS = Magnum::*Implementation \
# that contain example code fragments that are included (see the \include # that contain example code fragments that are included (see the \include
# command). # 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 # 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 # EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and
@ -913,6 +914,7 @@ EXAMPLE_RECURSIVE = NO
# \image command). # \image command).
IMAGE_PATH = doc/ \ IMAGE_PATH = doc/ \
doc/snippets/ \
../magnum-examples/src/ ../magnum-examples/src/
# The INPUT_FILTER tag can be used to specify a program that doxygen should # The INPUT_FILTER tag can be used to specify a program that doxygen should

40
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š <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()

27
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š <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}"

82
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š <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)

BIN
doc/snippets/debugtools-compareimage.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
doc/snippets/image1.tga

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
doc/snippets/image2.tga

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

13
src/Magnum/DebugTools/CMakeLists.txt

@ -43,6 +43,16 @@ if(MAGNUM_TARGET_GLES AND NOT MAGNUM_TARGET_GLES2)
list(APPEND MagnumDebugTools_SRCS ${MagnumDebugTools_RESOURCES}) list(APPEND MagnumDebugTools_SRCS ${MagnumDebugTools_RESOURCES})
endif() 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) if(NOT MAGNUM_TARGET_WEBGL)
list(APPEND MagnumDebugTools_SRCS list(APPEND MagnumDebugTools_SRCS
BufferData.cpp) BufferData.cpp)
@ -107,6 +117,9 @@ if(BUILD_STATIC_PIC)
endif() endif()
target_link_libraries(MagnumDebugTools Magnum) target_link_libraries(MagnumDebugTools Magnum)
if(Corrade_TestSuite_FOUND)
target_link_libraries(MagnumDebugTools Corrade::TestSuite)
endif()
if(WITH_SCENEGRAPH) if(WITH_SCENEGRAPH)
target_link_libraries(MagnumDebugTools MagnumSceneGraph) target_link_libraries(MagnumDebugTools MagnumSceneGraph)
endif() endif()

507
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š <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

173
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š <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

4
src/Magnum/DebugTools/Test/CMakeLists.txt

@ -33,6 +33,10 @@ if(WITH_SCENEGRAPH)
corrade_add_test(DebugToolsForceRendererTest ForceRendererTest.cpp LIBRARIES MagnumMathTestLib) corrade_add_test(DebugToolsForceRendererTest ForceRendererTest.cpp LIBRARIES MagnumMathTestLib)
endif() endif()
if(Corrade_TestSuite_FOUND)
corrade_add_test(DebugToolsCompareImageTest CompareImageTest.cpp LIBRARIES MagnumDebugTools)
endif()
if(BUILD_GL_TESTS) if(BUILD_GL_TESTS)
corrade_add_test(DebugToolsBufferDataGLTest BufferDataGLTest.cpp LIBRARIES MagnumDebugTools ${GL_TEST_LIBRARIES}) corrade_add_test(DebugToolsBufferDataGLTest BufferDataGLTest.cpp LIBRARIES MagnumDebugTools ${GL_TEST_LIBRARIES})
corrade_add_test(DebugToolsTextureImageGLTest TextureImageGLTest.cpp LIBRARIES MagnumDebugTools ${GL_TEST_LIBRARIES}) corrade_add_test(DebugToolsTextureImageGLTest TextureImageGLTest.cpp LIBRARIES MagnumDebugTools ${GL_TEST_LIBRARIES})

396
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š <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…
Cancel
Save