diff --git a/doc/changelog.dox b/doc/changelog.dox index 25acac4ec..73c207a78 100644 --- a/doc/changelog.dox +++ b/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 diff --git a/doc/snippets/MagnumDebugTools-gl.cpp b/doc/snippets/MagnumDebugTools-gl.cpp index 9ca354bf6..d1e64f660 100644 --- a/doc/snippets/MagnumDebugTools-gl.cpp +++ b/doc/snippets/MagnumDebugTools-gl.cpp @@ -23,14 +23,18 @@ DEALINGS IN THE SOFTWARE. */ +#include #include #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(image.pixels()), + "expected.png", DebugTools::CompareImageToFile); +/* [CompareImage-pixels-rgb] */ +} +} +}; diff --git a/doc/snippets/MagnumDebugTools.cpp b/doc/snippets/MagnumDebugTools.cpp index 7e5c49519..1b26c53e6 100644 --- a/doc/snippets/MagnumDebugTools.cpp +++ b/doc/snippets/MagnumDebugTools.cpp @@ -23,6 +23,7 @@ DEALINGS IN THE SOFTWARE. */ +#include #include #include @@ -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().flipped<0>(), expected, + (DebugTools::CompareImage{15.5f, 5.0f})); +/* [CompareImage-pixels-flip] */ +} } }; diff --git a/src/Magnum/DebugTools/CompareImage.cpp b/src/Magnum/DebugTools/CompareImage.cpp index 2bcaccdee..1b8f23382 100644 --- a/src/Magnum/DebugTools/CompareImage.cpp +++ b/src/Magnum/DebugTools/CompareImage.cpp @@ -91,13 +91,14 @@ template Float calculateImageDelta(const Containers:: } -std::tuple, Float, Float> calculateImageDelta(const ImageView2D& actual, const ImageView2D& expected) { +std::tuple, Float, Float> calculateImageDelta(const PixelFormat actualFormat, const Containers::StridedArrayView3D& actualPixels, const ImageView2D& expected) { /* Calculate a delta image */ Containers::Array deltaData{Containers::NoInit, std::size_t(expected.size().product())}; Containers::StridedArrayView2D 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, Float, Float> calculateImageDelta(const Ima #define _c(format, size, T) \ case PixelFormat::format: \ max = calculateImageDelta( \ - actual.pixels>(), \ + Containers::arrayCast<2, const Math::Vector>(actualPixels), \ expected.pixels>(), delta); \ break; #define _d(first, second, size, T) \ case PixelFormat::first: \ case PixelFormat::second: \ max = calculateImageDelta( \ - actual.pixels>(), \ + Containers::arrayCast<2, const Math::Vector>(actualPixels), \ expected.pixels>(), delta); \ break; /* LCOV_EXCL_START */ @@ -287,7 +288,7 @@ void printPixelAt(Debug& out, const Containers::StridedArrayView3D& } -void printPixelDeltas(Debug& out, Containers::ArrayView delta, const ImageView2D& actual, const ImageView2D& expected, const Float maxThreshold, const Float meanThreshold, std::size_t maxCount) { +void printPixelDeltas(Debug& out, Containers::ArrayView delta, PixelFormat format, const Containers::StridedArrayView3D& actualPixels, const Containers::StridedArrayView3D& 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 large; @@ -308,16 +309,16 @@ void printPixelDeltas(Debug& out, Containers::ArrayView 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 actualImageData, expectedImageData; - /** @todo could at least the views have a NoCreate constructor? */ - Containers::Optional actualImage, expectedImage; + PixelFormat actualFormat; + Containers::StridedArrayView3D actualPixels; + Containers::Optional expectedImage; Float maxThreshold, meanThreshold; Result result{}; @@ -385,26 +387,28 @@ ImageComparatorBase::ImageComparatorBase(PluginManager::Manager& 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 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& actualPixels, const std::string& expected) { _state->expectedFilename = expected; Containers::Pointer 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 data{_state->actualPixels.size()[0]*_state->actualPixels.size()[1]*_state->actualPixels.size()[2]}; + Containers::StridedArrayView3D pixels{data, _state->actualPixels.size()}; + for(std::size_t i = 0, iMax = _state->actualPixels.size()[0]; i != iMax; ++i) { + Containers::StridedArrayView2D inRow = _state->actualPixels[i]; + Containers::StridedArrayView2D outRow = pixels[i]; + for(std::size_t j = 0, jMax = inRow.size()[0]; j != jMax; ++j) { + Containers::StridedArrayView1D inPixel = inRow[j]; + Containers::StridedArrayView1D 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 converter = _state->converterManager().loadAndInstantiate("AnyImageConverter"); - if(converter && converter->exportToFile(*_state->actualImage, filename)) + if(converter && converter->exportToFile(image, filename)) out << "->" << filename; } diff --git a/src/Magnum/DebugTools/CompareImage.h b/src/Magnum/DebugTools/CompareImage.h index 69fa89b86..ec73f143f 100644 --- a/src/Magnum/DebugTools/CompareImage.h +++ b/src/Magnum/DebugTools/CompareImage.h @@ -36,6 +36,7 @@ #include #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, Float, Float> calculateImageDelta(const ImageView2D& actual, const ImageView2D& expected); + MAGNUM_DEBUGTOOLS_EXPORT std::tuple, Float, Float> calculateImageDelta(PixelFormat actualFormat, const Containers::StridedArrayView3D& actualPixels, const ImageView2D& expected); MAGNUM_DEBUGTOOLS_EXPORT void printDeltaImage(Debug& out, Containers::ArrayView delta, const Vector2i& size, Float max, Float maxThreshold, Float meanThreshold); - MAGNUM_DEBUGTOOLS_EXPORT void printPixelDeltas(Debug& out, Containers::ArrayView delta, const ImageView2D& actual, const ImageView2D& expected, Float maxThreshold, Float meanThreshold, std::size_t maxCount); + MAGNUM_DEBUGTOOLS_EXPORT void printPixelDeltas(Debug& out, Containers::ArrayView delta, PixelFormat format, const Containers::StridedArrayView3D& actualPixels, const Containers::StridedArrayView3D& expectedPixels, Float maxThreshold, Float meanThreshold, std::size_t maxCount); } class CompareImage; @@ -57,6 +58,8 @@ class CompareFileToImage; namespace Implementation { +template constexpr PixelFormat pixelFormatFor(); + class MAGNUM_DEBUGTOOLS_EXPORT ImageComparatorBase { public: explicit ImageComparatorBase(PluginManager::Manager* importerManager, PluginManager::Manager* 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& actualPixels, const ImageView2D& expected); + + /* Used in templated CompareImageToFile::operator() */ + TestSuite::ComparisonStatusFlags compare(PixelFormat actualFormat, const Containers::StridedArrayView3D& 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 StridedArrayView arrayCast(const StridedArrayView&); +} + +namespace TestSuite { template<> class MAGNUM_DEBUGTOOLS_EXPORT Comparator: public Magnum::DebugTools::Implementation::ImageComparatorBase { public: @@ -98,6 +115,13 @@ template<> class MAGNUM_DEBUGTOOLS_EXPORT Comparator TestSuite::ComparisonStatusFlags operator()(const Containers::StridedArrayView2D& actualPixels, const Magnum::ImageView2D& expected) { + /** @todo do some tryFindCompatibleFormat() here */ + return Magnum::DebugTools::Implementation::ImageComparatorBase::compare( + Magnum::DebugTools::Implementation::pixelFormatFor(), + Containers::arrayCast<3, const char>(actualPixels), expected); + } }; template<> class MAGNUM_DEBUGTOOLS_EXPORT Comparator: public Magnum::DebugTools::Implementation::ImageComparatorBase { @@ -120,6 +144,13 @@ template<> class MAGNUM_DEBUGTOOLS_EXPORT Comparator TestSuite::ComparisonStatusFlags operator()(const Containers::StridedArrayView2D& actualPixels, const std::string& expected) { + /** @todo do some tryFindCompatibleFormat() here */ + return Magnum::DebugTools::Implementation::ImageComparatorBase::compare( + Magnum::DebugTools::Implementation::pixelFormatFor(), + Containers::arrayCast<3, const char>(actualPixels), expected); + } }; template<> class MAGNUM_DEBUGTOOLS_EXPORT Comparator: public Magnum::DebugTools::Implementation::ImageComparatorBase { @@ -133,6 +164,36 @@ template<> class MAGNUM_DEBUGTOOLS_EXPORT Comparator::operator() is overloaded */ +template struct ComparatorTraits { + typedef Magnum::ImageView2D ActualType; + typedef Magnum::ImageView2D ExpectedType; +}; +template struct ComparatorTraits: ComparatorTraits {}; +template struct ComparatorTraits: ComparatorTraits {}; +template struct ComparatorTraits, U> { + typedef Containers::StridedArrayView2D ActualType; + typedef Magnum::ImageView2D ExpectedType; +}; + +/* Explicit ComparatorTraits specialization because + Comparator::operator() is overloaded */ +template struct ComparatorTraits { + typedef Magnum::ImageView2D ActualType; + typedef std::string ExpectedType; +}; +template struct ComparatorTraits: ComparatorTraits {}; +template struct ComparatorTraits: ComparatorTraits {}; +template struct ComparatorTraits, U> { + typedef Containers::StridedArrayView2D 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" 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 _c; }; +namespace Implementation { + +/* LCOV_EXCL_START */ +/* One-component types */ +template<> constexpr PixelFormat pixelFormatFor() { return PixelFormat::R8Unorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::R8Unorm; } +template<> constexpr PixelFormat pixelFormatFor() { return PixelFormat::R8Snorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::R8Snorm; } +template<> constexpr PixelFormat pixelFormatFor() { return PixelFormat::R16Unorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::R16Unorm; } +template<> constexpr PixelFormat pixelFormatFor() { return PixelFormat::R16Snorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::R16Snorm; } +template<> constexpr PixelFormat pixelFormatFor() { return PixelFormat::R32UI; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::R32UI; } +template<> constexpr PixelFormat pixelFormatFor() { return PixelFormat::R32I; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::R32I; } +template<> constexpr PixelFormat pixelFormatFor() { return PixelFormat::R32F; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::R32F; } + +/* Two-component types */ +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RG8Unorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RG8Unorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RG8Snorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RG8Snorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RG16Unorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RG16Unorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RG16Snorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RG16Snorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RG32UI; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RG32UI; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RG32I; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RG32I; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RG32F; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RG32F; } + +/* Three-component types */ +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGB8Unorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGB8Unorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGB8Unorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGB8Snorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGB8Snorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGB8Snorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGB16Unorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGB16Unorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGB16Unorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGB16Snorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGB16Snorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGB16Snorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGB32UI; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGB32UI; } +/* Skipping Math::Color3, as that isn't much used */ +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGB32I; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGB32I; } +/* Skipping Math::Color3, as that isn't much used */ +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGB32F; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGB32F; } + +/* Four-component types */ +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGBA8Unorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGBA8Unorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGBA8Unorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGBA8Snorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGBA8Snorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGBA8Snorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGBA16Unorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGBA16Unorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGBA16Unorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGBA16Snorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGBA16Snorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGBA16Snorm; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGBA32UI; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGBA32UI; } +/* Skipping Math::Color4, as that isn't much used */ +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGBA32I; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGBA32I; } +/* Skipping Math::Color4, as that isn't much used */ +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGBA32F; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGBA32F; } +template<> constexpr PixelFormat pixelFormatFor>() { return PixelFormat::RGBA32F; } +/* LCOV_EXCL_STOP */ + +} + }} #endif diff --git a/src/Magnum/DebugTools/Test/CompareImageTest.cpp b/src/Magnum/DebugTools/Test/CompareImageTest.cpp index 007e275bc..4d6029b0f 100644 --- a/src/Magnum/DebugTools/Test/CompareImageTest.cpp +++ b/src/Magnum/DebugTools/Test/CompareImageTest.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -102,6 +103,11 @@ struct CompareImageTest: TestSuite::Tester { void fileToImageActualLoadFailed(); void fileToImageActualIsCompressed(); + void pixelsToImage(); + void pixelsToImageError(); + void pixelsToFile(); + void pixelsToFileError(); + private: Containers::Optional> _importerManager; Containers::Optional> _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 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 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{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 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 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{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(), + ExpectedRgb, (CompareImage{40.0f, 20.0f})); + + /* No diagnostic as there's no error */ + TestSuite::Comparator 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 compare{20.0f, 10.0f}; + TestSuite::ComparisonStatusFlags flags = + compare(ActualRgb.pixels(), 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(), + Utility::Directory::join(DEBUGTOOLS_TEST_DIR, "CompareImageExpected.tga"), + (CompareImageToFile{*_importerManager, 40.0f, 20.0f})); + + /* No diagnostic as there's no error */ + TestSuite::Comparator 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 compare{&*_importerManager, &*_converterManager, 20.0f, 10.0f}; + TestSuite::ComparisonStatusFlags flags = compare(ActualRgb.pixels(), + 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)