Browse Source

DebugTools: CompareImage can now compare against pixel views.

This is getting too good.
pull/364/head
Vladimír Vondruš 7 years ago
parent
commit
a0232e8506
  1. 4
      doc/changelog.dox
  2. 19
      doc/snippets/MagnumDebugTools-gl.cpp
  3. 10
      doc/snippets/MagnumDebugTools.cpp
  4. 92
      src/Magnum/DebugTools/CompareImage.cpp
  5. 175
      src/Magnum/DebugTools/CompareImage.h
  6. 126
      src/Magnum/DebugTools/Test/CompareImageTest.cpp

4
doc/changelog.dox

@ -73,6 +73,10 @@ See also:
- @ref DebugTools::CompareImageFile and @ref DebugTools::CompareImageToFile
now support the new @ref TestSuite-Tester-save-diagnostic "--save-diagnostic option",
making it possible to save images when a comparison fails
- @ref DebugTools::CompareImage and @ref DebugTools::CompareImageToFile now
accept also @ref Corrade::Containers::StridedArrayView2D on the left side
of the comparison for added flexibility. See
@ref DebugTools-CompareImage-pixels for more infromation.
@subsubsection changelog-latest-new-gl GL library

19
doc/snippets/MagnumDebugTools-gl.cpp

@ -23,14 +23,18 @@
DEALINGS IN THE SOFTWARE.
*/
#include <Corrade/Containers/StridedArrayView.h>
#include <Corrade/TestSuite/Tester.h>
#include "Magnum/Image.h"
#include "Magnum/ImageView.h"
#include "Magnum/PixelFormat.h"
#include "Magnum/DebugTools/CompareImage.h"
#include "Magnum/DebugTools/ForceRenderer.h"
#include "Magnum/DebugTools/ResourceManager.h"
#include "Magnum/DebugTools/ObjectRenderer.h"
#include "Magnum/DebugTools/TextureImage.h"
#include "Magnum/GL/Framebuffer.h"
#include "Magnum/GL/CubeMapTexture.h"
#include "Magnum/GL/Texture.h"
#include "Magnum/Math/Range.h"
@ -134,3 +138,18 @@ GL::BufferImage2D image = DebugTools::textureSubImage(texture,
}
#endif
}
struct Foo: TestSuite::Tester {
void foo() {
{
GL::Framebuffer fb{{}};
ImageView2D expected{PixelFormat::RGB8Unorm, {}};
/* [CompareImage-pixels-rgb] */
Image2D image = fb.read(fb.viewport(), {PixelFormat::RGBA8Unorm});
CORRADE_COMPARE_AS(Containers::arrayCast<Color3ub>(image.pixels<Color4ub>()),
"expected.png", DebugTools::CompareImageToFile);
/* [CompareImage-pixels-rgb] */
}
}
};

10
doc/snippets/MagnumDebugTools.cpp

@ -23,6 +23,7 @@
DEALINGS IN THE SOFTWARE.
*/
#include <Corrade/Containers/StridedArrayView.h>
#include <Corrade/TestSuite/Tester.h>
#include <Corrade/PluginManager/Manager.h>
@ -88,5 +89,14 @@ CORRADE_COMPARE_WITH("actual.png", expected,
(DebugTools::CompareFileToImage{15.5f, 5.0f}));
/* [CompareFileToImage] */
}
{
Image2D actual = doProcessing();
Image2D expected = loadExpectedImage();
/* [CompareImage-pixels-flip] */
CORRADE_COMPARE_WITH(actual.pixels<Color3ub>().flipped<0>(), expected,
(DebugTools::CompareImage{15.5f, 5.0f}));
/* [CompareImage-pixels-flip] */
}
}
};

92
src/Magnum/DebugTools/CompareImage.cpp

@ -91,13 +91,14 @@ template<std::size_t size, class T> Float calculateImageDelta(const Containers::
}
std::tuple<Containers::Array<Float>, Float, Float> calculateImageDelta(const ImageView2D& actual, const ImageView2D& expected) {
std::tuple<Containers::Array<Float>, Float, Float> calculateImageDelta(const PixelFormat actualFormat, const Containers::StridedArrayView3D<const char>& actualPixels, const ImageView2D& expected) {
/* Calculate a delta image */
Containers::Array<Float> deltaData{Containers::NoInit,
std::size_t(expected.size().product())};
Containers::StridedArrayView2D<Float> delta{deltaData,
{std::size_t(expected.size().y()), std::size_t(expected.size().x())}};
CORRADE_INTERNAL_ASSERT(actualFormat == expected.format());
CORRADE_ASSERT(!isPixelFormatImplementationSpecific(expected.format()),
"DebugTools::CompareImage: can't compare implementation-specific pixel formats", {});
@ -106,14 +107,14 @@ std::tuple<Containers::Array<Float>, Float, Float> calculateImageDelta(const Ima
#define _c(format, size, T) \
case PixelFormat::format: \
max = calculateImageDelta<size, T>( \
actual.pixels<Math::Vector<size, T>>(), \
Containers::arrayCast<2, const Math::Vector<size, T>>(actualPixels), \
expected.pixels<Math::Vector<size, T>>(), delta); \
break;
#define _d(first, second, size, T) \
case PixelFormat::first: \
case PixelFormat::second: \
max = calculateImageDelta<size, T>( \
actual.pixels<Math::Vector<size, T>>(), \
Containers::arrayCast<2, const Math::Vector<size, T>>(actualPixels), \
expected.pixels<Math::Vector<size, T>>(), delta); \
break;
/* LCOV_EXCL_START */
@ -287,7 +288,7 @@ void printPixelAt(Debug& out, const Containers::StridedArrayView3D<const char>&
}
void printPixelDeltas(Debug& out, Containers::ArrayView<const Float> delta, const ImageView2D& actual, const ImageView2D& expected, const Float maxThreshold, const Float meanThreshold, std::size_t maxCount) {
void printPixelDeltas(Debug& out, Containers::ArrayView<const Float> delta, PixelFormat format, const Containers::StridedArrayView3D<const char>& actualPixels, const Containers::StridedArrayView3D<const char>& expectedPixels, const Float maxThreshold, const Float meanThreshold, std::size_t maxCount) {
/* Find first maxCount values above mean threshold and put them into a
sorted map. Need to reverse the condition in order to catch NaNs. */
std::multimap<Float, std::size_t> large;
@ -308,16 +309,16 @@ void printPixelDeltas(Debug& out, Containers::ArrayView<const Float> delta, cons
if(++count > maxCount) break;
Vector2i pos;
std::tie(pos.y(), pos.x()) = Math::div(Int(it->second), expected.size().x());
std::tie(pos.y(), pos.x()) = Math::div(Int(it->second), Int(expectedPixels.size()[1]));
out << Debug::newline << " [" << Debug::nospace << pos.x()
<< Debug::nospace << "," << Debug::nospace << pos.y()
<< Debug::nospace << "]";
printPixelAt(out, actual.pixels(), pos, expected.format());
printPixelAt(out, actualPixels, pos, format);
out << Debug::nospace << ", expected";
printPixelAt(out, expected.pixels(), pos, expected.format());
printPixelAt(out, expectedPixels, pos, format);
out << "(Δ =" << Debug::boldColor(delta[it->second] > maxThreshold ?
Debug::Color::Red : Debug::Color::Yellow) << delta[it->second]
@ -366,8 +367,9 @@ class ImageComparatorBase::State {
public:
std::string actualFilename, expectedFilename;
Containers::Optional<Trade::ImageData2D> actualImageData, expectedImageData;
/** @todo could at least the views have a NoCreate constructor? */
Containers::Optional<ImageView2D> actualImage, expectedImage;
PixelFormat actualFormat;
Containers::StridedArrayView3D<const char> actualPixels;
Containers::Optional<ImageView2D> expectedImage;
Float maxThreshold, meanThreshold;
Result result{};
@ -385,26 +387,28 @@ ImageComparatorBase::ImageComparatorBase(PluginManager::Manager<Trade::AbstractI
ImageComparatorBase::~ImageComparatorBase() = default;
TestSuite::ComparisonStatusFlags ImageComparatorBase::operator()(const ImageView2D& actual, const ImageView2D& expected) {
TestSuite::ComparisonStatusFlags ImageComparatorBase::compare(const PixelFormat actualFormat, const Containers::StridedArrayView3D<const char>& actualPixels, const ImageView2D& expected) {
/* The reference can be pointing to the storage, don't call the assignment
on itself in that case */
if(!_state->actualImage || &*_state->actualImage != &actual)
_state->actualImage = actual;
if(&_state->actualPixels != &actualPixels) {
_state->actualFormat = actualFormat;
_state->actualPixels = actualPixels;
}
if(!_state->expectedImage || &*_state->expectedImage != &expected)
_state->expectedImage = expected;
/* Verify that the images are the same */
if(actual.size() != expected.size()) {
if(Vector2i{Int(actualPixels.size()[1]), Int(actualPixels.size()[0])} != expected.size()) {
_state->result = Result::DifferentSize;
return TestSuite::ComparisonStatusFlag::Failed;
}
if(actual.format() != expected.format()) {
if(actualFormat != expected.format()) {
_state->result = Result::DifferentFormat;
return TestSuite::ComparisonStatusFlag::Failed;
}
Containers::Array<Float> delta;
std::tie(delta, _state->max, _state->mean) = DebugTools::Implementation::calculateImageDelta(actual, expected);
std::tie(delta, _state->max, _state->mean) = DebugTools::Implementation::calculateImageDelta(actualFormat, actualPixels, expected);
/* Verify the max/mean is never below zero so we didn't mess up when
calculating specials. Note the inverted condition to catch NaNs in
@ -428,6 +432,10 @@ TestSuite::ComparisonStatusFlags ImageComparatorBase::operator()(const ImageView
return TestSuite::ComparisonStatusFlag::Failed;
}
TestSuite::ComparisonStatusFlags ImageComparatorBase::operator()(const ImageView2D& actual, const ImageView2D& expected) {
return compare(actual.format(), actual.pixels(), expected);
}
TestSuite::ComparisonStatusFlags ImageComparatorBase::operator()(const std::string& actual, const std::string& expected) {
_state->actualFilename = actual;
_state->expectedFilename = expected;
@ -460,7 +468,8 @@ TestSuite::ComparisonStatusFlags ImageComparatorBase::operator()(const std::stri
so save also the view on its parsed contents to avoid it going out of
scope. We're saving through an image converter, not the original file,
see saveDiagnostic() for reasons why. */
_state->actualImage.emplace(*_state->actualImageData);
_state->actualFormat = _state->actualImageData->format();
_state->actualPixels = _state->actualImageData->pixels();
/* If the expected file can't be opened, we should still be able to save
the actual as a diagnostic. This could get also used to generate ground
@ -480,13 +489,13 @@ TestSuite::ComparisonStatusFlags ImageComparatorBase::operator()(const std::stri
/* Save also a view on the expected image data and proxy to the actual data
comparison. If comparison failed, offer to save a diagnostic. */
_state->expectedImage.emplace(*_state->expectedImageData);
TestSuite::ComparisonStatusFlags flags = operator()(*_state->actualImage, *_state->expectedImage);
TestSuite::ComparisonStatusFlags flags = compare(_state->actualFormat, _state->actualPixels, *_state->expectedImage);
if(flags & TestSuite::ComparisonStatusFlag::Failed)
flags |= TestSuite::ComparisonStatusFlag::Diagnostic;
return flags;
}
TestSuite::ComparisonStatusFlags ImageComparatorBase::operator()(const ImageView2D& actual, const std::string& expected) {
TestSuite::ComparisonStatusFlags ImageComparatorBase::compare(const PixelFormat actualFormat, const Containers::StridedArrayView3D<const char>& actualPixels, const std::string& expected) {
_state->expectedFilename = expected;
Containers::Pointer<Trade::AbstractImporter> importer;
@ -499,8 +508,13 @@ TestSuite::ComparisonStatusFlags ImageComparatorBase::operator()(const ImageView
}
/* Save the actual image so saveDiagnostic() can reach the data even if we
fail before the final data comparison (which does this as well) */
_state->actualImage = actual;
fail before the final data comparison (which does this as well). The
reference can be pointing to the storage, don't call the assignment on
itself in that case. */
if(&_state->actualPixels != &actualPixels) {
_state->actualFormat = actualFormat;
_state->actualPixels = actualPixels;
}
/* If the expected file can't be opened, we should still be able to save
the actual as a diagnostic. This could get also used to generate ground
@ -520,12 +534,16 @@ TestSuite::ComparisonStatusFlags ImageComparatorBase::operator()(const ImageView
/* Save a view on the expected image data and proxy to the actual data
comparison. If comparison failed, offer to save a diagnostic. */
_state->expectedImage.emplace(*_state->expectedImageData);
TestSuite::ComparisonStatusFlags flags = operator()(actual, *_state->expectedImage);
TestSuite::ComparisonStatusFlags flags = compare(_state->actualFormat, _state->actualPixels, *_state->expectedImage);
if(flags & TestSuite::ComparisonStatusFlag::Failed)
flags |= TestSuite::ComparisonStatusFlag::Diagnostic;
return flags;
}
TestSuite::ComparisonStatusFlags ImageComparatorBase::operator()(const ImageView2D& actual, const std::string& expected) {
return compare(actual.format(), actual.pixels(), expected);
}
TestSuite::ComparisonStatusFlags ImageComparatorBase::operator()(const std::string& actual, const ImageView2D& expected) {
_state->actualFilename = actual;
@ -549,8 +567,9 @@ TestSuite::ComparisonStatusFlags ImageComparatorBase::operator()(const std::stri
return TestSuite::ComparisonStatusFlag::Failed;
}
_state->actualImage.emplace(*_state->actualImageData);
return operator()(*_state->actualImage, expected);
_state->actualFormat = _state->actualImageData->format();
_state->actualPixels = _state->actualImageData->pixels();
return compare(_state->actualFormat, _state->actualPixels, expected);
}
void ImageComparatorBase::printMessage(TestSuite::ComparisonStatusFlags, Debug& out, const std::string& actual, const std::string& expected) const {
@ -577,10 +596,11 @@ void ImageComparatorBase::printMessage(TestSuite::ComparisonStatusFlags, Debug&
out << "Images" << actual << "and" << expected << "have";
if(_state->result == Result::DifferentSize)
out << "different size, actual" << _state->actualImage->size()
out << "different size, actual"
<< Vector2i{Int(_state->actualPixels.size()[1]), Int(_state->actualPixels.size()[0])}
<< "but" << _state->expectedImage->size() << "expected.";
else if(_state->result == Result::DifferentFormat)
out << "different format, actual" << _state->actualImage->format()
out << "different format, actual" << _state->actualFormat
<< "but" << _state->expectedImage->format() << "expected.";
else {
if(_state->result == Result::AboveThresholds)
@ -602,20 +622,36 @@ void ImageComparatorBase::printMessage(TestSuite::ComparisonStatusFlags, Debug&
out << "Delta image:" << Debug::newline;
DebugTools::Implementation::printDeltaImage(out, _state->delta, _state->expectedImage->size(), _state->max, _state->maxThreshold, _state->meanThreshold);
DebugTools::Implementation::printPixelDeltas(out, _state->delta, *_state->actualImage, *_state->expectedImage, _state->maxThreshold, _state->meanThreshold, 10);
CORRADE_INTERNAL_ASSERT(_state->actualFormat == _state->expectedImage->format());
DebugTools::Implementation::printPixelDeltas(out, _state->delta, _state->actualFormat, _state->actualPixels, _state->expectedImage->pixels(), _state->maxThreshold, _state->meanThreshold, 10);
}
}
void ImageComparatorBase::saveDiagnostic(TestSuite::ComparisonStatusFlags, Utility::Debug& out, const std::string& path) {
CORRADE_INTERNAL_ASSERT(_state->actualImage);
/* Tightly pack the actual pixels into a new array and create an image from
it -- the array view might have totally arbitrary strides that can't
be represented in an Image */
Containers::Array<char> data{_state->actualPixels.size()[0]*_state->actualPixels.size()[1]*_state->actualPixels.size()[2]};
Containers::StridedArrayView3D<char> pixels{data, _state->actualPixels.size()};
for(std::size_t i = 0, iMax = _state->actualPixels.size()[0]; i != iMax; ++i) {
Containers::StridedArrayView2D<const char> inRow = _state->actualPixels[i];
Containers::StridedArrayView2D<char> outRow = pixels[i];
for(std::size_t j = 0, jMax = inRow.size()[0]; j != jMax; ++j) {
Containers::StridedArrayView1D<const char> inPixel = inRow[j];
Containers::StridedArrayView1D<char> outPixel = outRow[j];
for(std::size_t k = 0, kMax = inPixel.size(); k != kMax; ++k)
outPixel[k] = inPixel[k];
}
}
const ImageView2D image{PixelStorage{}.setAlignment(1), _state->actualFormat, Vector2i{Int(pixels.size()[1]), Int(pixels.size()[0])}, data};
const std::string filename = Utility::Directory::join(path, Utility::Directory::filename(_state->expectedFilename));
/* Export the data the base view/view comparator saved. Ignore failures,
we're in the middle of a fail anyway (and everything will print messages
to the output nevertheless). */
Containers::Pointer<Trade::AbstractImageConverter> converter = _state->converterManager().loadAndInstantiate("AnyImageConverter");
if(converter && converter->exportToFile(*_state->actualImage, filename))
if(converter && converter->exportToFile(image, filename))
out << "->" << filename;
}

175
src/Magnum/DebugTools/CompareImage.h

@ -36,6 +36,7 @@
#include <Corrade/Utility/StlForwardTuple.h>
#include "Magnum/Magnum.h"
#include "Magnum/PixelFormat.h"
#include "Magnum/Math/Vector2.h"
#include "Magnum/DebugTools/visibility.h"
#include "Magnum/Trade/Trade.h"
@ -43,11 +44,11 @@
namespace Magnum { namespace DebugTools {
namespace Implementation {
MAGNUM_DEBUGTOOLS_EXPORT std::tuple<Containers::Array<Float>, Float, Float> calculateImageDelta(const ImageView2D& actual, const ImageView2D& expected);
MAGNUM_DEBUGTOOLS_EXPORT std::tuple<Containers::Array<Float>, Float, Float> calculateImageDelta(PixelFormat actualFormat, const Containers::StridedArrayView3D<const char>& actualPixels, const ImageView2D& expected);
MAGNUM_DEBUGTOOLS_EXPORT void printDeltaImage(Debug& out, Containers::ArrayView<const Float> delta, const Vector2i& size, Float max, Float maxThreshold, Float meanThreshold);
MAGNUM_DEBUGTOOLS_EXPORT void printPixelDeltas(Debug& out, Containers::ArrayView<const Float> delta, const ImageView2D& actual, const ImageView2D& expected, Float maxThreshold, Float meanThreshold, std::size_t maxCount);
MAGNUM_DEBUGTOOLS_EXPORT void printPixelDeltas(Debug& out, Containers::ArrayView<const Float> delta, PixelFormat format, const Containers::StridedArrayView3D<const char>& actualPixels, const Containers::StridedArrayView3D<const char>& expectedPixels, Float maxThreshold, Float meanThreshold, std::size_t maxCount);
}
class CompareImage;
@ -57,6 +58,8 @@ class CompareFileToImage;
namespace Implementation {
template<class> constexpr PixelFormat pixelFormatFor();
class MAGNUM_DEBUGTOOLS_EXPORT ImageComparatorBase {
public:
explicit ImageComparatorBase(PluginManager::Manager<Trade::AbstractImporter>* importerManager, PluginManager::Manager<Trade::AbstractImageConverter>* converterManager, Float maxThreshold, Float meanThreshold);
@ -73,6 +76,12 @@ class MAGNUM_DEBUGTOOLS_EXPORT ImageComparatorBase {
TestSuite::ComparisonStatusFlags operator()(const ImageView2D& actual, const std::string& expected);
/* Used in templated CompareImage::operator() */
TestSuite::ComparisonStatusFlags compare(PixelFormat actualFormat, const Containers::StridedArrayView3D<const char>& actualPixels, const ImageView2D& expected);
/* Used in templated CompareImageToFile::operator() */
TestSuite::ComparisonStatusFlags compare(PixelFormat actualFormat, const Containers::StridedArrayView3D<const char>& actualPixels, const std::string& expected);
void printMessage(TestSuite::ComparisonStatusFlags flags, Debug& out, const std::string& actual, const std::string& expected) const;
void saveDiagnostic(TestSuite::ComparisonStatusFlags flags, Utility::Debug& out, const std::string& path);
@ -87,7 +96,15 @@ class MAGNUM_DEBUGTOOLS_EXPORT ImageComparatorBase {
#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 {
namespace Corrade {
namespace Containers {
/* Forward-declaring this function to avoid the need to include
the whole StridedArrayView */
template<unsigned newDimensions, class U, unsigned dimensions, class T> StridedArrayView<newDimensions, U> arrayCast(const StridedArrayView<dimensions, T>&);
}
namespace TestSuite {
template<> class MAGNUM_DEBUGTOOLS_EXPORT Comparator<Magnum::DebugTools::CompareImage>: public Magnum::DebugTools::Implementation::ImageComparatorBase {
public:
@ -98,6 +115,13 @@ template<> class MAGNUM_DEBUGTOOLS_EXPORT Comparator<Magnum::DebugTools::Compare
ComparisonStatusFlags operator()(const Magnum::ImageView2D& actual, const Magnum::ImageView2D& expected) {
return Magnum::DebugTools::Implementation::ImageComparatorBase::operator()(actual, expected);
}
template<class T> TestSuite::ComparisonStatusFlags operator()(const Containers::StridedArrayView2D<const T>& actualPixels, const Magnum::ImageView2D& expected) {
/** @todo do some tryFindCompatibleFormat() here */
return Magnum::DebugTools::Implementation::ImageComparatorBase::compare(
Magnum::DebugTools::Implementation::pixelFormatFor<T>(),
Containers::arrayCast<3, const char>(actualPixels), expected);
}
};
template<> class MAGNUM_DEBUGTOOLS_EXPORT Comparator<Magnum::DebugTools::CompareImageFile>: public Magnum::DebugTools::Implementation::ImageComparatorBase {
@ -120,6 +144,13 @@ template<> class MAGNUM_DEBUGTOOLS_EXPORT Comparator<Magnum::DebugTools::Compare
ComparisonStatusFlags operator()(const Magnum::ImageView2D& actual, const std::string& expected) {
return Magnum::DebugTools::Implementation::ImageComparatorBase::operator()(actual, expected);
}
template<class T> TestSuite::ComparisonStatusFlags operator()(const Containers::StridedArrayView2D<const T>& actualPixels, const std::string& expected) {
/** @todo do some tryFindCompatibleFormat() here */
return Magnum::DebugTools::Implementation::ImageComparatorBase::compare(
Magnum::DebugTools::Implementation::pixelFormatFor<T>(),
Containers::arrayCast<3, const char>(actualPixels), expected);
}
};
template<> class MAGNUM_DEBUGTOOLS_EXPORT Comparator<Magnum::DebugTools::CompareFileToImage>: public Magnum::DebugTools::Implementation::ImageComparatorBase {
@ -133,6 +164,36 @@ template<> class MAGNUM_DEBUGTOOLS_EXPORT Comparator<Magnum::DebugTools::Compare
}
};
namespace Implementation {
/* Explicit ComparatorTraits specialization because
Comparator<CompareImage>::operator() is overloaded */
template<class T> struct ComparatorTraits<Magnum::DebugTools::CompareImage, Magnum::ImageView2D, T> {
typedef Magnum::ImageView2D ActualType;
typedef Magnum::ImageView2D ExpectedType;
};
template<class T> struct ComparatorTraits<Magnum::DebugTools::CompareImage, Magnum::Image2D, T>: ComparatorTraits<Magnum::DebugTools::CompareImage, Magnum::ImageView2D, T> {};
template<class T> struct ComparatorTraits<Magnum::DebugTools::CompareImage, Magnum::Trade::ImageData2D, T>: ComparatorTraits<Magnum::DebugTools::CompareImage, Magnum::ImageView2D, T> {};
template<class T, class U> struct ComparatorTraits<Magnum::DebugTools::CompareImage, Containers::StridedArrayView2D<T>, U> {
typedef Containers::StridedArrayView2D<const T> ActualType;
typedef Magnum::ImageView2D ExpectedType;
};
/* Explicit ComparatorTraits specialization because
Comparator<CompareImageToFile>::operator() is overloaded */
template<class T> struct ComparatorTraits<Magnum::DebugTools::CompareImageToFile, Magnum::ImageView2D, T> {
typedef Magnum::ImageView2D ActualType;
typedef std::string ExpectedType;
};
template<class T> struct ComparatorTraits<Magnum::DebugTools::CompareImageToFile, Magnum::Image2D, T>: ComparatorTraits<Magnum::DebugTools::CompareImageToFile, Magnum::ImageView2D, T> {};
template<class T> struct ComparatorTraits<Magnum::DebugTools::CompareImageToFile, Magnum::Trade::ImageData2D, T>: ComparatorTraits<Magnum::DebugTools::CompareImageToFile, Magnum::ImageView2D, T> {};
template<class T, class U> struct ComparatorTraits<Magnum::DebugTools::CompareImageToFile, Containers::StridedArrayView2D<T>, U> {
typedef Containers::StridedArrayView2D<const T> ActualType;
typedef std::string ExpectedType;
};
}
}}
#endif
@ -222,6 +283,31 @@ For floating-point input, the comparator treats the values similarly to how
For the ASCII-art representation, NaN and infinity @f$ \Delta_{\boldsymbol{p}} @f$
values are always treated as maximum difference.
@section DebugTools-CompareImage-pixels Comparing against pixel views
For added flexibility, it's possible to use a
@ref Corrade::Containers::StridedArrayView2D containing pixel data on the left
side of the comparison in both @ref CompareImage and @ref CompareImageToFile.
This type is commonly returned from @ref ImageView::pixels() and allows you to
do arbitrary operations on the viewed data --- for example, comparing pixel
data flipped upside down:
@snippet MagnumDebugTools.cpp CompareImage-pixels-flip
For a different scenario, imagine you're comparing data read from a framebuffer
to a ground truth image. On many systems, internal framebuffer storage has to
be four-component; however your if your ground truth image is just
three-component you can cast the pixel data to just a three-component type:
@snippet MagnumDebugTools-gl.cpp CompareImage-pixels-rgb
Currently, comparing against pixel views has a few inherent limitations --- it
has to be cast to one of Magnum scalar or vector types and the format is
then autodetected from the passed type, with normalized formats preferred. In
practice this means e.g. @ref Math::Vector2 "Math::Vector2<UnsignedByte>" will
be understood as @ref PixelFormat::RG8Unorm and there's currently no way to
interpret it as @ref PixelFormat::RG8UI, for example.
*/
class CompareImage {
public:
@ -476,6 +562,89 @@ class CompareFileToImage {
TestSuite::Comparator<CompareFileToImage> _c;
};
namespace Implementation {
/* LCOV_EXCL_START */
/* One-component types */
template<> constexpr PixelFormat pixelFormatFor<UnsignedByte>() { return PixelFormat::R8Unorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<1, UnsignedByte>>() { return PixelFormat::R8Unorm; }
template<> constexpr PixelFormat pixelFormatFor<Byte>() { return PixelFormat::R8Snorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<1, Byte>>() { return PixelFormat::R8Snorm; }
template<> constexpr PixelFormat pixelFormatFor<UnsignedShort>() { return PixelFormat::R16Unorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<1, UnsignedShort>>() { return PixelFormat::R16Unorm; }
template<> constexpr PixelFormat pixelFormatFor<Short>() { return PixelFormat::R16Snorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<1, Short>>() { return PixelFormat::R16Snorm; }
template<> constexpr PixelFormat pixelFormatFor<UnsignedInt>() { return PixelFormat::R32UI; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<1, UnsignedInt>>() { return PixelFormat::R32UI; }
template<> constexpr PixelFormat pixelFormatFor<Int>() { return PixelFormat::R32I; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<1, Int>>() { return PixelFormat::R32I; }
template<> constexpr PixelFormat pixelFormatFor<Float>() { return PixelFormat::R32F; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<1, Float>>() { return PixelFormat::R32F; }
/* Two-component types */
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<2, UnsignedByte>>() { return PixelFormat::RG8Unorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector2<UnsignedByte>>() { return PixelFormat::RG8Unorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<2, Byte>>() { return PixelFormat::RG8Snorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector2<Byte>>() { return PixelFormat::RG8Snorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<2, UnsignedShort>>() { return PixelFormat::RG16Unorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector2<UnsignedShort>>() { return PixelFormat::RG16Unorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<2, Short>>() { return PixelFormat::RG16Snorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector2<Short>>() { return PixelFormat::RG16Snorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<2, UnsignedInt>>() { return PixelFormat::RG32UI; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector2<UnsignedInt>>() { return PixelFormat::RG32UI; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<2, Int>>() { return PixelFormat::RG32I; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector2<Int>>() { return PixelFormat::RG32I; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<2, Float>>() { return PixelFormat::RG32F; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector2<Float>>() { return PixelFormat::RG32F; }
/* Three-component types */
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<3, UnsignedByte>>() { return PixelFormat::RGB8Unorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector3<UnsignedByte>>() { return PixelFormat::RGB8Unorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Color3<UnsignedByte>>() { return PixelFormat::RGB8Unorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<3, Byte>>() { return PixelFormat::RGB8Snorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector3<Byte>>() { return PixelFormat::RGB8Snorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Color3<Byte>>() { return PixelFormat::RGB8Snorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<3, UnsignedShort>>() { return PixelFormat::RGB16Unorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector3<UnsignedShort>>() { return PixelFormat::RGB16Unorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Color3<UnsignedShort>>() { return PixelFormat::RGB16Unorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<3, Short>>() { return PixelFormat::RGB16Snorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector3<Short>>() { return PixelFormat::RGB16Snorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Color3<Short>>() { return PixelFormat::RGB16Snorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<3, UnsignedInt>>() { return PixelFormat::RGB32UI; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector3<UnsignedInt>>() { return PixelFormat::RGB32UI; }
/* Skipping Math::Color3<UnsignedInt>, as that isn't much used */
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<3, Int>>() { return PixelFormat::RGB32I; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector3<Int>>() { return PixelFormat::RGB32I; }
/* Skipping Math::Color3<Int>, as that isn't much used */
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<3, Float>>() { return PixelFormat::RGB32F; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector3<Float>>() { return PixelFormat::RGB32F; }
/* Four-component types */
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<4, UnsignedByte>>() { return PixelFormat::RGBA8Unorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector4<UnsignedByte>>() { return PixelFormat::RGBA8Unorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Color4<UnsignedByte>>() { return PixelFormat::RGBA8Unorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<4, Byte>>() { return PixelFormat::RGBA8Snorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector4<Byte>>() { return PixelFormat::RGBA8Snorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Color4<Byte>>() { return PixelFormat::RGBA8Snorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<4, UnsignedShort>>() { return PixelFormat::RGBA16Unorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector4<UnsignedShort>>() { return PixelFormat::RGBA16Unorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Color4<UnsignedShort>>() { return PixelFormat::RGBA16Unorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<4, Short>>() { return PixelFormat::RGBA16Snorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector4<Short>>() { return PixelFormat::RGBA16Snorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Color4<Short>>() { return PixelFormat::RGBA16Snorm; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<4, UnsignedInt>>() { return PixelFormat::RGBA32UI; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector4<UnsignedInt>>() { return PixelFormat::RGBA32UI; }
/* Skipping Math::Color4<UnsignedInt>, as that isn't much used */
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<4, Int>>() { return PixelFormat::RGBA32I; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector4<Int>>() { return PixelFormat::RGBA32I; }
/* Skipping Math::Color4<Int>, as that isn't much used */
template<> constexpr PixelFormat pixelFormatFor<Math::Vector<4, Float>>() { return PixelFormat::RGBA32F; }
template<> constexpr PixelFormat pixelFormatFor<Math::Vector4<Float>>() { return PixelFormat::RGBA32F; }
template<> constexpr PixelFormat pixelFormatFor<Math::Color4<Float>>() { return PixelFormat::RGBA32F; }
/* LCOV_EXCL_STOP */
}
}}
#endif

126
src/Magnum/DebugTools/Test/CompareImageTest.cpp

@ -26,6 +26,7 @@
#include <sstream>
#include <numeric>
#include <Corrade/Containers/Array.h>
#include <Corrade/Containers/StridedArrayView.h>
#include <Corrade/Containers/Optional.h>
#include <Corrade/PluginManager/Manager.h>
#include <Corrade/TestSuite/Tester.h>
@ -102,6 +103,11 @@ struct CompareImageTest: TestSuite::Tester {
void fileToImageActualLoadFailed();
void fileToImageActualIsCompressed();
void pixelsToImage();
void pixelsToImageError();
void pixelsToFile();
void pixelsToFileError();
private:
Containers::Optional<PluginManager::Manager<Trade::AbstractImporter>> _importerManager;
Containers::Optional<PluginManager::Manager<Trade::AbstractImageConverter>> _converterManager;
@ -180,6 +186,14 @@ CompareImageTest::CompareImageTest() {
addTests({&CompareImageTest::fileToImageActualIsCompressed});
addTests({&CompareImageTest::pixelsToImage,
&CompareImageTest::pixelsToImageError});
addTests({&CompareImageTest::pixelsToFile,
&CompareImageTest::pixelsToFileError},
&CompareImageTest::setupExternalPluginManager,
&CompareImageTest::teardownExternalPluginManager);
/* Plugin manager setup is not done here, but in the
setupExternalPluginManager() function */
}
@ -210,7 +224,7 @@ void CompareImageTest::formatUnknown() {
Error redirectError{&out};
ImageView2D image{PixelStorage{}, PixelFormat(0xdead), 0, 0, {}};
Implementation::calculateImageDelta(image, image);
Implementation::calculateImageDelta(image.format(), image.pixels(), image);
CORRADE_COMPARE(out.str(), "DebugTools::CompareImage: unknown format PixelFormat(0xdead)\n");
}
@ -220,7 +234,7 @@ void CompareImageTest::formatHalf() {
Error redirectError{&out};
ImageView2D image{PixelFormat::RG16F, {}};
Implementation::calculateImageDelta(image, image);
Implementation::calculateImageDelta(image.format(), image.pixels(), image);
CORRADE_COMPARE(out.str(), "DebugTools::CompareImage: half-float formats are not supported yet\n");
}
@ -230,7 +244,7 @@ void CompareImageTest::formatImplementationSpecific() {
Error redirectError{&out};
ImageView2D image{PixelStorage{}, pixelFormatWrap(0xdead), 0, 0, {}};
Implementation::calculateImageDelta(image, image);
Implementation::calculateImageDelta(image.format(), image.pixels(), image);
CORRADE_COMPARE(out.str(), "DebugTools::CompareImage: can't compare implementation-specific pixel formats\n");
}
@ -238,7 +252,7 @@ void CompareImageTest::formatImplementationSpecific() {
void CompareImageTest::calculateDelta() {
Containers::Array<Float> delta;
Float max, mean;
std::tie(delta, max, mean) = Implementation::calculateImageDelta(ActualRed, ExpectedRed);
std::tie(delta, max, mean) = Implementation::calculateImageDelta(ActualRed.format(), ActualRed.pixels(), ExpectedRed);
CORRADE_COMPARE_AS(delta, Containers::arrayView(DeltaRed), TestSuite::Compare::Container);
CORRADE_COMPARE(max, 1.0f);
@ -266,7 +280,7 @@ const ImageView2D ExpectedRgb{
void CompareImageTest::calculateDeltaStorage() {
Containers::Array<Float> delta;
Float max, mean;
std::tie(delta, max, mean) = Implementation::calculateImageDelta(ActualRgb, ExpectedRgb);
std::tie(delta, max, mean) = Implementation::calculateImageDelta(ActualRgb.format(), ActualRgb.pixels(), ExpectedRgb);
CORRADE_COMPARE_AS(delta, (Containers::Array<Float>{Containers::InPlaceInit, {
1.0f/3.0f, (55.0f + 1.0f)/3.0f,
@ -310,7 +324,7 @@ const ImageView2D ExpectedSpecials{PixelFormat::R32F, {9, 1}, ExpectedDataSpecia
void CompareImageTest::calculateDeltaSpecials() {
Containers::Array<Float> delta;
Float max, mean;
std::tie(delta, max, mean) = Implementation::calculateImageDelta(ActualSpecials, ExpectedSpecials);
std::tie(delta, max, mean) = Implementation::calculateImageDelta(ActualSpecials.format(), ActualSpecials.pixels(), ExpectedSpecials);
CORRADE_COMPARE_AS(Containers::arrayView(delta),
Containers::arrayView(DeltaSpecials),
TestSuite::Compare::Container);
@ -331,7 +345,7 @@ void CompareImageTest::calculateDeltaSpecials3() {
Containers::Array<Float> delta;
Float max, mean;
std::tie(delta, max, mean) = Implementation::calculateImageDelta(actualSpecials3, expectedSpecials3);
std::tie(delta, max, mean) = Implementation::calculateImageDelta(actualSpecials3.format(), actualSpecials3.pixels(), expectedSpecials3);
CORRADE_COMPARE_AS(delta, (Containers::Array<Float>{Containers::InPlaceInit, {
Constants::nan(), Constants::nan(), 1.15f
}}), TestSuite::Compare::Container);
@ -436,12 +450,12 @@ 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);
Implementation::printPixelDeltas(d, DeltaRed, ActualRed.format(), ActualRed.pixels(), ExpectedRed.pixels(), 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);
Implementation::printPixelDeltas(d, DeltaRed, ActualRed.format(), ActualRed.pixels(), ExpectedRed.pixels(), 0.5f, 0.1f, 10);
CORRADE_COMPARE(out.str(),
" Pixels above max/mean threshold:\n"
@ -454,7 +468,7 @@ void CompareImageTest::pixelDelta() {
void CompareImageTest::pixelDeltaOverflow() {
std::ostringstream out;
Debug d{&out, Debug::Flag::DisableColors};
Implementation::printPixelDeltas(d, DeltaRed, ActualRed, ExpectedRed, 0.5f, 0.1f, 3);
Implementation::printPixelDeltas(d, DeltaRed, ActualRed.format(), ActualRed.pixels(), ExpectedRed.pixels(), 0.5f, 0.1f, 3);
CORRADE_COMPARE(out.str(),
" Top 3 out of 4 pixels above max/mean threshold:\n"
@ -466,7 +480,7 @@ void CompareImageTest::pixelDeltaOverflow() {
void CompareImageTest::pixelDeltaSpecials() {
std::ostringstream out;
Debug d{&out, Debug::Flag::DisableColors};
Implementation::printPixelDeltas(d, DeltaSpecials, ActualSpecials, ExpectedSpecials, 1.5f, 0.5f, 10);
Implementation::printPixelDeltas(d, DeltaSpecials, ActualSpecials.format(), ActualSpecials.pixels(), ExpectedSpecials.pixels(), 1.5f, 0.5f, 10);
/* MSVC prints -nan(ind) instead of ±nan. But only sometimes. */
#ifdef _MSC_VER
@ -1232,6 +1246,96 @@ void CompareImageTest::fileToImageActualIsCompressed() {
"Actual image a (.../CompareImageCompressed.dds) is compressed, comparison not possible.\n");
}
void CompareImageTest::pixelsToImage() {
/* Same as image(), but taking pixels instead */
CORRADE_COMPARE_WITH(ActualRgb.pixels<Color3ub>(),
ExpectedRgb, (CompareImage{40.0f, 20.0f}));
/* No diagnostic as there's no error */
TestSuite::Comparator<CompareImage> compare{40.0f, 20.0f};
CORRADE_COMPARE(compare(ActualRgb, ExpectedRgb), TestSuite::ComparisonStatusFlags{});
}
void CompareImageTest::pixelsToImageError() {
/* Same as imageError(), but taking pixels instead */
std::stringstream out;
{
TestSuite::Comparator<CompareImage> compare{20.0f, 10.0f};
TestSuite::ComparisonStatusFlags flags =
compare(ActualRgb.pixels<Color3ub>(), ExpectedRgb);
/* No diagnostic as we don't have any expected filename */
CORRADE_COMPARE(flags, TestSuite::ComparisonStatusFlag::Failed);
Debug d{&out, Debug::Flag::DisableColors};
compare.printMessage(flags, d, "a", "b");
}
CORRADE_COMPARE(out.str(), ImageCompareError);
}
void CompareImageTest::pixelsToFile() {
/* Same as imageToFile(), but taking pixels instead */
if(_importerManager->loadState("AnyImageImporter") == PluginManager::LoadState::NotFound ||
_importerManager->loadState("TgaImporter") == PluginManager::LoadState::NotFound)
CORRADE_SKIP("AnyImageImporter or TgaImporter plugins not found.");
CORRADE_COMPARE_WITH(ActualRgb.pixels<Color3ub>(),
Utility::Directory::join(DEBUGTOOLS_TEST_DIR, "CompareImageExpected.tga"),
(CompareImageToFile{*_importerManager, 40.0f, 20.0f}));
/* No diagnostic as there's no error */
TestSuite::Comparator<CompareImageToFile> compare{&*_importerManager, nullptr, 40.0f, 20.0f};
CORRADE_COMPARE(compare(ActualRgb, Utility::Directory::join(DEBUGTOOLS_TEST_DIR, "CompareImageExpected.tga")),
TestSuite::ComparisonStatusFlags{});
}
void CompareImageTest::pixelsToFileError() {
/* Same as imageToFileError(), but taking pixels instead */
if(_importerManager->loadState("AnyImageImporter") == PluginManager::LoadState::NotFound ||
_importerManager->loadState("TgaImporter") == PluginManager::LoadState::NotFound)
CORRADE_SKIP("AnyImageImporter or TgaImporter plugins not found.");
std::stringstream out;
TestSuite::Comparator<CompareImageToFile> compare{&*_importerManager, &*_converterManager, 20.0f, 10.0f};
TestSuite::ComparisonStatusFlags flags = compare(ActualRgb.pixels<Color3ub>(),
Utility::Directory::join(DEBUGTOOLS_TEST_DIR, "CompareImageExpected.tga"));
/* The diagnostic flag should be slapped on the failure coming from the
operator() comparing two ImageViews */
CORRADE_COMPARE(flags, TestSuite::ComparisonStatusFlag::Failed|TestSuite::ComparisonStatusFlag::Diagnostic);
{
Debug d{&out, Debug::Flag::DisableColors};
compare.printMessage(flags, d, "a", "b");
}
CORRADE_COMPARE(out.str(), ImageCompareError);
/* Create the output dir if it doesn't exist, but avoid stale files making
false positives */
CORRADE_VERIFY(Utility::Directory::mkpath(COMPAREIMAGETEST_SAVE_DIR));
std::string filename = Utility::Directory::join(COMPAREIMAGETEST_SAVE_DIR, "CompareImageExpected.tga");
if(Utility::Directory::exists(filename))
CORRADE_VERIFY(Utility::Directory::rm(filename));
{
out.str({});
Debug redirectOutput(&out);
compare.saveDiagnostic(flags, redirectOutput, COMPAREIMAGETEST_SAVE_DIR);
}
/* We expect the *actual* contents, but under the *expected* filename.
Comparing file contents, expecting the converter makes exactly the same
file. */
CORRADE_COMPARE(out.str(), Utility::formatString("-> {}\n", filename));
CORRADE_COMPARE_AS(filename,
Utility::Directory::join(DEBUGTOOLS_TEST_DIR, "CompareImageActual.tga"), TestSuite::Compare::File);
}
}}}}
CORRADE_TEST_MAIN(Magnum::DebugTools::Test::CompareImageTest)

Loading…
Cancel
Save