Browse Source

DebugTools: PIMPL all CompareImage's state.

The distinction didn't make sense and we had to include more than
necessary. It's great how easy those refactorings are with the 100% code
coverage.
pull/364/head
Vladimír Vondruš 7 years ago
parent
commit
c6737e1958
  1. 213
      src/Magnum/DebugTools/CompareImage.cpp
  2. 16
      src/Magnum/DebugTools/CompareImage.h
  3. 1
      src/Magnum/DebugTools/Test/CompareImageTest.cpp

213
src/Magnum/DebugTools/CompareImage.cpp

@ -27,9 +27,11 @@
#include <map>
#include <sstream>
#include <Corrade/Containers/Array.h>
#include <Corrade/Containers/StridedArrayView.h>
#include <Corrade/Containers/Optional.h>
#include <Corrade/PluginManager/Manager.h>
#include <Corrade/TestSuite/Comparator.h>
#include <Corrade/Utility/DebugStl.h>
#include <Corrade/Utility/Directory.h>
@ -323,7 +325,7 @@ void printPixelDeltas(Debug& out, Containers::ArrayView<const Float> delta, cons
}
}
enum class ImageComparatorBase::State: UnsignedByte {
enum class Result: UnsignedByte {
PluginLoadFailed = 1,
ActualImageLoadFailed,
ExpectedImageLoadFailed,
@ -336,11 +338,9 @@ enum class ImageComparatorBase::State: UnsignedByte {
AboveMaxThreshold
};
class ImageComparatorBase::FileState {
class ImageComparatorBase::State {
public:
explicit FileState(PluginManager::Manager<Trade::AbstractImporter>* importerManager, PluginManager::Manager<Trade::AbstractImageConverter>* converterManager): _importerManager{importerManager}, _converterManager{converterManager} {}
explicit FileState() {}
explicit State(PluginManager::Manager<Trade::AbstractImporter>* importerManager, PluginManager::Manager<Trade::AbstractImageConverter>* converterManager, Float maxThreshold, Float meanThreshold): _importerManager{importerManager}, _converterManager{converterManager}, maxThreshold{maxThreshold}, meanThreshold{meanThreshold} {}
/* Lazy-create the importer / converter if those weren't passed from
the outside. The importer might not be used at all if we are
@ -368,14 +368,14 @@ class ImageComparatorBase::FileState {
Containers::Optional<Trade::ImageData2D> actualImageData, expectedImageData;
/** @todo could at least the views have a NoCreate constructor? */
Containers::Optional<ImageView2D> actualImage, expectedImage;
};
ImageComparatorBase::ImageComparatorBase(PluginManager::Manager<Trade::AbstractImporter>* importerManager, PluginManager::Manager<Trade::AbstractImageConverter>* converterManager, Float maxThreshold, Float meanThreshold): _maxThreshold{maxThreshold}, _meanThreshold{meanThreshold}, _max{}, _mean{} {
/* Only instantiate the file state if there's something to save -- if we
are comparing two image data, it won't be used at all */
if(importerManager || converterManager)
_fileState.reset(new FileState{importerManager, converterManager});
Float maxThreshold, meanThreshold;
Result result{};
Float max{}, mean{};
Containers::Array<Float> delta;
};
ImageComparatorBase::ImageComparatorBase(PluginManager::Manager<Trade::AbstractImporter>* importerManager, PluginManager::Manager<Trade::AbstractImageConverter>* converterManager, Float maxThreshold, Float meanThreshold): _state{Containers::InPlaceInit, importerManager, converterManager, maxThreshold, meanThreshold} {
CORRADE_ASSERT(!Math::isNan(maxThreshold) && !Math::isInf(maxThreshold) &&
!Math::isNan(meanThreshold) && !Math::isInf(meanThreshold),
"DebugTools::CompareImage: thresholds can't be NaN or infinity", );
@ -386,75 +386,73 @@ ImageComparatorBase::ImageComparatorBase(PluginManager::Manager<Trade::AbstractI
ImageComparatorBase::~ImageComparatorBase() = default;
TestSuite::ComparisonStatusFlags ImageComparatorBase::operator()(const ImageView2D& actual, const ImageView2D& expected) {
/* Taking just references is okay because in case of files those are
references to actual views stored inside FileState; and in case of
data, the actual/expected params passed here stay in scope for the whole
time where operator(), printMessage() & saveDiagnostic() gets called */
_actualImage = &actual;
_expectedImage = &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->expectedImage || &*_state->expectedImage != &expected)
_state->expectedImage = expected;
/* Verify that the images are the same */
if(actual.size() != expected.size()) {
_state = State::DifferentSize;
_state->result = Result::DifferentSize;
return TestSuite::ComparisonStatusFlag::Failed;
}
if(actual.format() != expected.format()) {
_state = State::DifferentFormat;
_state->result = Result::DifferentFormat;
return TestSuite::ComparisonStatusFlag::Failed;
}
Containers::Array<Float> delta;
std::tie(delta, _max, _mean) = DebugTools::Implementation::calculateImageDelta(actual, expected);
std::tie(delta, _state->max, _state->mean) = DebugTools::Implementation::calculateImageDelta(actual, 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
_mean. The max should OTOH be never special as it would make all other
deltas become zero in comparison. */
CORRADE_INTERNAL_ASSERT(!(_mean < 0.0f));
CORRADE_INTERNAL_ASSERT(_max >= 0.0f && !Math::isInf(_max) && !Math::isNan(_max));
CORRADE_INTERNAL_ASSERT(!(_state->mean < 0.0f));
CORRADE_INTERNAL_ASSERT(_state->max >= 0.0f && !Math::isInf(_state->max) && !Math::isNan(_state->max));
/* If both values are not above threshold, success. Comparing this way in
order to properly catch NaNs in mean values. */
if(_max > _maxThreshold && !(_mean <= _meanThreshold))
_state = State::AboveThresholds;
else if(_max > _maxThreshold)
_state = State::AboveMaxThreshold;
else if(!(_mean <= _meanThreshold))
_state = State::AboveMeanThreshold;
if(_state->max > _state->maxThreshold && !(_state->mean <= _state->meanThreshold))
_state->result = Result::AboveThresholds;
else if(_state->max > _state->maxThreshold)
_state->result = Result::AboveMaxThreshold;
else if(!(_state->mean <= _state->meanThreshold))
_state->result = Result::AboveMeanThreshold;
else return TestSuite::ComparisonStatusFlags{};
/* Otherwise save the deltas and fail */
_delta = std::move(delta);
_state->delta = std::move(delta);
return TestSuite::ComparisonStatusFlag::Failed;
}
TestSuite::ComparisonStatusFlags ImageComparatorBase::operator()(const std::string& actual, const std::string& expected) {
if(!_fileState) _fileState.reset(new FileState);
_fileState->actualFilename = actual;
_fileState->expectedFilename = expected;
_state->actualFilename = actual;
_state->expectedFilename = expected;
Containers::Pointer<Trade::AbstractImporter> importer;
/* Can't load importer plugin. While we *could* save diagnostic in this
case too, it would make no sense as it's a Schrödinger image at this
point -- we have no idea if it's the same or not until we open it. */
if(!(importer = _fileState->importerManager().loadAndInstantiate("AnyImageImporter"))) {
_state = State::PluginLoadFailed;
if(!(importer = _state->importerManager().loadAndInstantiate("AnyImageImporter"))) {
_state->result = Result::PluginLoadFailed;
return TestSuite::ComparisonStatusFlag::Failed;
}
/* Same here. We can't open the image for some reason (file missing? broken
plugin?), so can't know if it's the same or not. */
if(!importer->openFile(actual) || !(_fileState->actualImageData = importer->image2D(0))) {
_state = State::ActualImageLoadFailed;
if(!importer->openFile(actual) || !(_state->actualImageData = importer->image2D(0))) {
_state->result = Result::ActualImageLoadFailed;
return TestSuite::ComparisonStatusFlag::Failed;
}
/* If the actual data are compressed, we won't be able to compare them
(and probably neither save them back due to format mismatches). Don't
provide diagnostic in that case. */
if(_fileState->actualImageData->isCompressed()) {
_state = State::ActualImageIsCompressed;
if(_state->actualImageData->isCompressed()) {
_state->result = Result::ActualImageIsCompressed;
return TestSuite::ComparisonStatusFlag::Failed;
}
@ -462,173 +460,162 @@ 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. */
_fileState->actualImage.emplace(*_fileState->actualImageData);
/* Save a reference to the actual image so saveDiagnostic() can reach the
data even if we fail before the final data comparison (which does this
as well) */
_actualImage = &*_fileState->actualImage;
_state->actualImage.emplace(*_state->actualImageData);
/* 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
truth data on the first-ever test run. */
if(!importer->openFile(expected) || !(_fileState->expectedImageData = importer->image2D(0))) {
_state = State::ExpectedImageLoadFailed;
if(!importer->openFile(expected) || !(_state->expectedImageData = importer->image2D(0))) {
_state->result = Result::ExpectedImageLoadFailed;
return TestSuite::ComparisonStatusFlag::Failed|TestSuite::ComparisonStatusFlag::Diagnostic;
}
/* If the expected file is compressed, it's bad, but it doesn't mean we
couldn't save the actual file either */
if(_fileState->expectedImageData->isCompressed()) {
_state = State::ExpectedImageIsCompressed;
if(_state->expectedImageData->isCompressed()) {
_state->result = Result::ExpectedImageIsCompressed;
return TestSuite::ComparisonStatusFlag::Failed|TestSuite::ComparisonStatusFlag::Diagnostic;
}
/* Save also a view on the expected image data and proxy to the actual data
comparison. If comparison failed, offer to save a diagnostic. */
_fileState->expectedImage.emplace(*_fileState->expectedImageData);
TestSuite::ComparisonStatusFlags flags = operator()(*_fileState->actualImage, *_fileState->expectedImage);
_state->expectedImage.emplace(*_state->expectedImageData);
TestSuite::ComparisonStatusFlags flags = operator()(*_state->actualImage, *_state->expectedImage);
if(flags & TestSuite::ComparisonStatusFlag::Failed)
flags |= TestSuite::ComparisonStatusFlag::Diagnostic;
return flags;
}
TestSuite::ComparisonStatusFlags ImageComparatorBase::operator()(const ImageView2D& actual, const std::string& expected) {
if(!_fileState) _fileState.reset(new FileState);
_fileState->expectedFilename = expected;
_state->expectedFilename = expected;
Containers::Pointer<Trade::AbstractImporter> importer;
/* Can't load importer plugin. While we *could* save diagnostic in this
case too, it would make no sense as it's a Schrödinger image at this
point -- we have no idea if it's the same or not until we open it. */
if(!(importer = _fileState->importerManager().loadAndInstantiate("AnyImageImporter"))) {
_state = State::PluginLoadFailed;
if(!(importer = _state->importerManager().loadAndInstantiate("AnyImageImporter"))) {
_state->result = Result::PluginLoadFailed;
return TestSuite::ComparisonStatusFlag::Failed;
}
/* Save a reference to the actual image so saveDiagnostic() can reach the
data even if we fail before the final data comparison (which does this
as well) */
_actualImage = &actual;
/* 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;
/* 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
truth data on the first-ever test run. */
if(!importer->openFile(expected) || !(_fileState->expectedImageData = importer->image2D(0))) {
_state = State::ExpectedImageLoadFailed;
if(!importer->openFile(expected) || !(_state->expectedImageData = importer->image2D(0))) {
_state->result = Result::ExpectedImageLoadFailed;
return TestSuite::ComparisonStatusFlag::Failed|TestSuite::ComparisonStatusFlag::Diagnostic;
}
/* If the expected file is compressed, it's bad, but it doesn't mean we
couldn't save the actual file either */
if(_fileState->expectedImageData->isCompressed()) {
_state = State::ExpectedImageIsCompressed;
if(_state->expectedImageData->isCompressed()) {
_state->result = Result::ExpectedImageIsCompressed;
return TestSuite::ComparisonStatusFlag::Failed|TestSuite::ComparisonStatusFlag::Diagnostic;
}
/* Save a view on the expected image data and proxy to the actual data
comparison. If comparison failed, offer to save a diagnostic. */
_fileState->expectedImage.emplace(*_fileState->expectedImageData);
TestSuite::ComparisonStatusFlags flags = operator()(actual, *_fileState->expectedImage);
_state->expectedImage.emplace(*_state->expectedImageData);
TestSuite::ComparisonStatusFlags flags = operator()(actual, *_state->expectedImage);
if(flags & TestSuite::ComparisonStatusFlag::Failed)
flags |= TestSuite::ComparisonStatusFlag::Diagnostic;
return flags;
}
TestSuite::ComparisonStatusFlags ImageComparatorBase::operator()(const std::string& actual, const ImageView2D& expected) {
if(!_fileState) _fileState.reset(new FileState);
_fileState->actualFilename = actual;
_state->actualFilename = actual;
/* Here we are comparing against a view, not a file, so we cannot save
diagnostic in any case as we don't have the expected filename. This
behavior is consistent with TestSuite::Compare::FileToString. */
Containers::Pointer<Trade::AbstractImporter> importer;
if(!(importer = _fileState->importerManager().loadAndInstantiate("AnyImageImporter"))) {
_state = State::PluginLoadFailed;
if(!(importer = _state->importerManager().loadAndInstantiate("AnyImageImporter"))) {
_state->result = Result::PluginLoadFailed;
return TestSuite::ComparisonStatusFlag::Failed;
}
if(!importer->openFile(actual) || !(_fileState->actualImageData = importer->image2D(0))) {
_state = State::ActualImageLoadFailed;
if(!importer->openFile(actual) || !(_state->actualImageData = importer->image2D(0))) {
_state->result = Result::ActualImageLoadFailed;
return TestSuite::ComparisonStatusFlag::Failed;
}
if(_fileState->actualImageData->isCompressed()) {
_state = State::ActualImageIsCompressed;
if(_state->actualImageData->isCompressed()) {
_state->result = Result::ActualImageIsCompressed;
return TestSuite::ComparisonStatusFlag::Failed;
}
_fileState->actualImage.emplace(*_fileState->actualImageData);
return operator()(*_fileState->actualImage, expected);
_state->actualImage.emplace(*_state->actualImageData);
return operator()(*_state->actualImage, expected);
}
void ImageComparatorBase::printMessage(TestSuite::ComparisonStatusFlags, Debug& out, const std::string& actual, const std::string& expected) const {
if(_state == State::PluginLoadFailed) {
if(_state->result == Result::PluginLoadFailed) {
out << "AnyImageImporter plugin could not be loaded.";
return;
}
if(_state == State::ActualImageLoadFailed) {
out << "Actual image" << actual << "(" << Debug::nospace << _fileState->actualFilename << Debug::nospace << ")" << "could not be loaded.";
if(_state->result == Result::ActualImageLoadFailed) {
out << "Actual image" << actual << "(" << Debug::nospace << _state->actualFilename << Debug::nospace << ")" << "could not be loaded.";
return;
}
if(_state == State::ExpectedImageLoadFailed) {
out << "Expected image" << expected << "(" << Debug::nospace << _fileState->expectedFilename << Debug::nospace << ")" << "could not be loaded.";
if(_state->result == Result::ExpectedImageLoadFailed) {
out << "Expected image" << expected << "(" << Debug::nospace << _state->expectedFilename << Debug::nospace << ")" << "could not be loaded.";
return;
}
if(_state == State::ActualImageIsCompressed) {
out << "Actual image" << actual << "(" << Debug::nospace << _fileState->actualFilename << Debug::nospace << ")" << "is compressed, comparison not possible.";
if(_state->result == Result::ActualImageIsCompressed) {
out << "Actual image" << actual << "(" << Debug::nospace << _state->actualFilename << Debug::nospace << ")" << "is compressed, comparison not possible.";
return;
}
if(_state == State::ExpectedImageIsCompressed) {
out << "Expected image" << expected << "(" << Debug::nospace << _fileState->expectedFilename << Debug::nospace << ")" << "is compressed, comparison not possible.";
if(_state->result == Result::ExpectedImageIsCompressed) {
out << "Expected image" << expected << "(" << Debug::nospace << _state->expectedFilename << Debug::nospace << ")" << "is compressed, comparison not possible.";
return;
}
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() << "but"
<< _expectedImage->format() << "expected.";
if(_state->result == Result::DifferentSize)
out << "different size, actual" << _state->actualImage->size()
<< "but" << _state->expectedImage->size() << "expected.";
else if(_state->result == Result::DifferentFormat)
out << "different format, actual" << _state->actualImage->format()
<< "but" << _state->expectedImage->format() << "expected.";
else {
if(_state == State::AboveThresholds)
if(_state->result == Result::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 << ".";
<< _state->max << Debug::nospace << "/" << Debug::nospace << _state->mean
<< "but at most" << _state->maxThreshold << Debug::nospace << "/"
<< Debug::nospace << _state->meanThreshold << "expected.";
else if(_state->result == Result::AboveMaxThreshold)
out << "max delta above threshold, actual" << _state->max
<< "but at most" << _state->maxThreshold
<< "expected. Mean delta" << _state->mean << "is below threshold"
<< _state->meanThreshold << Debug::nospace << ".";
else if(_state->result == Result::AboveMeanThreshold)
out << "mean delta above threshold, actual" << _state->mean
<< "but at most" << _state->meanThreshold
<< "expected. Max delta" << _state->max << "is below threshold"
<< _state->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);
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);
}
}
void ImageComparatorBase::saveDiagnostic(TestSuite::ComparisonStatusFlags, Utility::Debug& out, const std::string& path) {
CORRADE_INTERNAL_ASSERT(_fileState);
CORRADE_INTERNAL_ASSERT(_actualImage);
CORRADE_INTERNAL_ASSERT(_state->actualImage);
const std::string filename = Utility::Directory::join(path, Utility::Directory::filename(_fileState->expectedFilename));
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 = _fileState->converterManager().loadAndInstantiate("AnyImageConverter");
if(converter && converter->exportToFile(*_actualImage, filename))
Containers::Pointer<Trade::AbstractImageConverter> converter = _state->converterManager().loadAndInstantiate("AnyImageConverter");
if(converter && converter->exportToFile(*_state->actualImage, filename))
out << "->" << filename;
}

16
src/Magnum/DebugTools/CompareImage.h

@ -29,10 +29,9 @@
* @brief Class @ref Magnum::DebugTools::CompareImage
*/
#include <Corrade/Containers/Array.h>
#include <Corrade/Containers/Pointer.h>
#include <Corrade/PluginManager/PluginManager.h>
#include <Corrade/TestSuite/Comparator.h>
#include <Corrade/TestSuite/TestSuite.h>
#include <Corrade/Utility/StlForwardString.h>
#include <Corrade/Utility/StlForwardTuple.h>
@ -79,17 +78,8 @@ class MAGNUM_DEBUGTOOLS_EXPORT ImageComparatorBase {
void saveDiagnostic(TestSuite::ComparisonStatusFlags flags, Utility::Debug& out, const std::string& path);
private:
class MAGNUM_DEBUGTOOLS_LOCAL FileState;
enum class State: UnsignedByte;
Containers::Pointer<FileState> _fileState;
Float _maxThreshold, _meanThreshold;
State _state{};
const ImageView2D *_actualImage{}, *_expectedImage{};
Float _max, _mean;
Containers::Array<Float> _delta;
class MAGNUM_DEBUGTOOLS_LOCAL State;
Containers::Pointer<State> _state;
};
}}}

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

@ -25,6 +25,7 @@
#include <sstream>
#include <numeric>
#include <Corrade/Containers/Array.h>
#include <Corrade/Containers/Optional.h>
#include <Corrade/PluginManager/Manager.h>
#include <Corrade/TestSuite/Tester.h>

Loading…
Cancel
Save