diff --git a/include/eepp/graphics/image.hpp b/include/eepp/graphics/image.hpp index bceaf6e60..1c2b543a2 100644 --- a/include/eepp/graphics/image.hpp +++ b/include/eepp/graphics/image.hpp @@ -432,7 +432,7 @@ class EE_API Image { const FormatConfiguration& getImageFormatConfiguration() const; struct DiffResult { - Image* diffImage{ nullptr }; ///< The visual diff image. Null if dimensions mismatched. + Image* diffImage{ nullptr }; ///< Null when visual output was disabled or cannot be created. long long numDifferentPixels{ 0 }; ///< The number of pixels that exceeded the threshold. double maxDeltaE{ 0.0 }; ///< The maximum perceptual difference (Delta E) found. bool areSame() const { return numDifferentPixels == 0; } @@ -450,10 +450,13 @@ class EE_API Image { * A value of 1.0 is roughly the limit of human perception. * A common default for tests is 2.3 (a "just noticeable difference"). * @param diffColor The color used to highlight differing pixels in the output image. + * @param createDiffImage Whether to create the visual diff image. Disable this for comparisons + * that only need statistics. * @return A DiffResult struct containing the diff image and statistics. */ DiffResult diff( const Image& other, float threshold = 2.3f, - const Color& diffColor = Color( 255, 0, 255, 255 ) ) const; + const Color& diffColor = Color( 255, 0, 255, 255 ), + bool createDiffImage = true ) const; protected: Uint8* mPixels; diff --git a/src/eepp/graphics/image.cpp b/src/eepp/graphics/image.cpp index 3823ac3e3..dc606c194 100644 --- a/src/eepp/graphics/image.cpp +++ b/src/eepp/graphics/image.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -36,16 +37,24 @@ struct Lab { double l, a, b; }; -// sRGB to Linear RGB conversion -static constexpr double srgbToLinear( double c ) { - return ( c > 0.04045 ) ? pow( ( c + 0.055 ) / 1.055, 2.4 ) : ( c / 12.92 ); +static const std::array& getSRGBToLinearLUT() { + static const std::array lut = [] { + std::array values{}; + for ( size_t i = 0; i < values.size(); ++i ) { + const double c = static_cast( i ) / 255.0; + values[i] = c > 0.04045 ? pow( ( c + 0.055 ) / 1.055, 2.4 ) : c / 12.92; + } + return values; + }(); + return lut; } // RGB to XYZ conversion (D65 illuminant) -static constexpr XYZ rgbToXyz( const Color& c ) { - double r = srgbToLinear( c.r / 255.0 ); - double g = srgbToLinear( c.g / 255.0 ); - double b = srgbToLinear( c.b / 255.0 ); +static XYZ rgbToXyz( const Color& c ) { + const auto& lut = getSRGBToLinearLUT(); + double r = lut[c.r]; + double g = lut[c.g]; + double b = lut[c.b]; return { .x = r * 0.4124564 + g * 0.3575761 + b * 0.1804375, .y = r * 0.2126729 + g * 0.7151522 + b * 0.0721750, @@ -1510,7 +1519,8 @@ std::pair, int> Image::loadGif( IOStream& stream ) { return { std::move( gif ), delay ? delay : 100 }; } -Image::DiffResult Image::diff( const Image& other, float threshold, const Color& diffColor ) const { +Image::DiffResult Image::diff( const Image& other, float threshold, const Color& diffColor, + bool createDiffImage ) const { DiffResult result; if ( getWidth() != other.getWidth() || getHeight() != other.getHeight() ) { @@ -1525,59 +1535,69 @@ Image::DiffResult Image::diff( const Image& other, float threshold, const Color& return result; } - // Create a 4-channel RGBA image for the diff output - result.diffImage = Image::New( getWidth(), getHeight(), 4 ); - if ( !result.diffImage ) { - Log::error( "Image::diff: Failed to create diff image." ); - return result; + if ( createDiffImage ) { + // Create a 4-channel RGBA image for the diff output + result.diffImage = Image::New( getWidth(), getHeight(), 4 ); + if ( !result.diffImage ) { + Log::error( "Image::diff: Failed to create diff image." ); + return result; + } } - const Uint8* p1 = this->getPixelsPtr(); - const Uint8* p2 = other.getPixelsPtr(); - Uint8* pDiff = result.diffImage->getPixels(); + const size_t pixelCount = + static_cast( getWidth() ) * static_cast( getHeight() ); + const unsigned int channels1 = getChannels(); + const unsigned int channels2 = other.getChannels(); + const Uint8* pixel1 = getPixelsPtr(); + const Uint8* pixel2 = other.getPixelsPtr(); + Uint8* diffPixel = result.diffImage ? result.diffImage->getPixels() : nullptr; - unsigned int channels1 = this->getChannels(); - unsigned int channels2 = other.getChannels(); - unsigned int channelsDiff = result.diffImage->getChannels(); + for ( size_t i = 0; i < pixelCount; ++i ) { + const Uint8 r1 = pixel1[0]; + const Uint8 g1 = pixel1[1]; + const Uint8 b1 = pixel1[2]; + const Uint8 r2 = pixel2[0]; + const Uint8 g2 = pixel2[1]; + const Uint8 b2 = pixel2[2]; + const Uint8 a1 = channels1 == 4 ? pixel1[3] : 255; + const Uint8 a2 = channels2 == 4 ? pixel2[3] : 255; + const bool rgbEqual = r1 == r2 && g1 == g2 && b1 == b2; + const bool alphaDiffers = a1 != a2; + double delta = 0.0; - for ( unsigned int y = 0; y < getHeight(); ++y ) { - for ( unsigned int x = 0; x < getWidth(); ++x ) { - size_t offset1 = ( y * getWidth() + x ) * channels1; - size_t offset2 = ( y * getWidth() + x ) * channels2; - size_t offsetDiff = ( y * getWidth() + x ) * channelsDiff; + if ( !rgbEqual ) { + const Color c1( r1, g1, b1, a1 ); + const Color c2( r2, g2, b2, a2 ); + const Lab lab1 = xyzToLab( rgbToXyz( c1 ) ); + const Lab lab2 = xyzToLab( rgbToXyz( c2 ) ); + delta = deltaECIEDE2000( lab1, lab2 ); + result.maxDeltaE = eemax( result.maxDeltaE, delta ); + } - Color c1( p1[offset1], p1[offset1 + 1], p1[offset1 + 2], - channels1 == 4 ? p1[offset1 + 3] : 255 ); - Color c2( p2[offset2], p2[offset2 + 1], p2[offset2 + 2], - channels2 == 4 ? p2[offset2 + 3] : 255 ); + const bool differs = delta > threshold || alphaDiffers; + if ( differs ) + ++result.numDifferentPixels; - // Also compare alpha channels directly, as perceptual diff is for color only - bool alphaDiffers = ( c1.a != c2.a ); - - Lab lab1 = xyzToLab( rgbToXyz( c1 ) ); - Lab lab2 = xyzToLab( rgbToXyz( c2 ) ); - double delta = deltaECIEDE2000( lab1, lab2 ); - - if ( delta > result.maxDeltaE ) { - result.maxDeltaE = delta; - } - - if ( delta > threshold || alphaDiffers ) { - result.numDifferentPixels++; - pDiff[offsetDiff] = diffColor.r; - pDiff[offsetDiff + 1] = diffColor.g; - pDiff[offsetDiff + 2] = diffColor.b; - pDiff[offsetDiff + 3] = diffColor.a; + if ( diffPixel ) { + if ( differs ) { + diffPixel[0] = diffColor.r; + diffPixel[1] = diffColor.g; + diffPixel[2] = diffColor.b; + diffPixel[3] = diffColor.a; } else { // Blend original pixel with a transparent gray to fade it out // This makes the highlighted differences stand out more. - Uint8 avg = ( c1.r + c1.g + c1.b ) / 3; - pDiff[offsetDiff] = avg; - pDiff[offsetDiff + 1] = avg; - pDiff[offsetDiff + 2] = avg; - pDiff[offsetDiff + 3] = 128; // Semi-transparent + const Uint8 avg = ( r1 + g1 + b1 ) / 3; + diffPixel[0] = avg; + diffPixel[1] = avg; + diffPixel[2] = avg; + diffPixel[3] = 128; // Semi-transparent } + diffPixel += 4; } + + pixel1 += channels1; + pixel2 += channels2; } return result; diff --git a/src/tests/unit_tests/compareimages.hpp b/src/tests/unit_tests/compareimages.hpp index 3c6907387..aaf1c91ac 100644 --- a/src/tests/unit_tests/compareimages.hpp +++ b/src/tests/unit_tests/compareimages.hpp @@ -40,9 +40,13 @@ static void compareImages( utest_state_s& utest_state, int* utest_result, EE::Wi EXPECT_EQ_MSG( expectedImage.getWidth(), actualImage.getWidth(), "Images width not equal" ); EXPECT_EQ_MSG( expectedImage.getHeight(), actualImage.getHeight(), "Images height not equal" ); - Image::DiffResult result = actualImage.diff( expectedImage ); + constexpr float diffThreshold = 2.3f; + const Color diffColor( 255, 0, 255, 255 ); + Image::DiffResult result = actualImage.diff( expectedImage, diffThreshold, diffColor, false ); EXPECT_LE( result.numDifferentPixels, allowedNumDifferentPixels ); if ( imageSizeMismatch || result.numDifferentPixels > allowedNumDifferentPixels ) { + Image::DiffResult visualResult = + actualImage.diff( expectedImage, diffThreshold, diffColor, true ); auto saveExt( Image::saveTypeToExtension( saveType ) ); std::string withTextShaper = Text::TextShaperEnabled @@ -112,11 +116,11 @@ static void compareImages( utest_state_s& utest_state, int* utest_result, EE::Wi "output/" + imageName + "_actual_output" + withTextShaper + "." + saveExt; actualImage.saveToFile( actualImagePath, saveType ); std::cerr << "Actual image saved to: " << actualImagePath << std::endl; - if ( result.diffImage ) { + if ( visualResult.diffImage ) { std::string diffImagePath = "output/" + imageName + "_diff_output" + withTextShaper + "." + saveExt; - result.diffImage->setImageFormatConfiguration( fconf ); - result.diffImage->saveToFile( diffImagePath, saveType ); + visualResult.diffImage->setImageFormatConfiguration( fconf ); + visualResult.diffImage->saveToFile( diffImagePath, saveType ); std::cerr << "Visual diff saved to: " << diffImagePath << std::endl; } } diff --git a/src/tests/unit_tests/image_diff_tests.cpp b/src/tests/unit_tests/image_diff_tests.cpp new file mode 100644 index 000000000..e5666e626 --- /dev/null +++ b/src/tests/unit_tests/image_diff_tests.cpp @@ -0,0 +1,97 @@ +#include "utest.h" + +#include + +using namespace EE; +using namespace EE::Graphics; + +UTEST( ImageDiff, ExactEqualityRGB ) { + const Uint8 pixels[] = { 12, 34, 56, 78, 90, 123 }; + const Image image1( pixels, 2, 1, 3 ); + const Image image2( pixels, 2, 1, 3 ); + + Image::DiffResult visualResult = image1.diff( image2 ); + EXPECT_EQ( visualResult.numDifferentPixels, 0 ); + EXPECT_EQ( visualResult.maxDeltaE, 0.0 ); + ASSERT_TRUE( visualResult.diffImage != nullptr ); + + Image::DiffResult compareResult = image1.diff( image2, 2.3f, Color( 255, 0, 255, 255 ), false ); + EXPECT_EQ( compareResult.numDifferentPixels, 0 ); + EXPECT_EQ( compareResult.maxDeltaE, 0.0 ); + EXPECT_TRUE( compareResult.diffImage == nullptr ); +} + +UTEST( ImageDiff, ExactEqualityRGBA ) { + const Uint8 pixels[] = { 12, 34, 56, 78 }; + const Image image1( pixels, 1, 1, 4 ); + const Image image2( pixels, 1, 1, 4 ); + + Image::DiffResult visualResult = image1.diff( image2 ); + EXPECT_EQ( visualResult.numDifferentPixels, 0 ); + EXPECT_EQ( visualResult.maxDeltaE, 0.0 ); + ASSERT_TRUE( visualResult.diffImage != nullptr ); + + Image::DiffResult compareResult = image1.diff( image2, 2.3f, Color( 255, 0, 255, 255 ), false ); + EXPECT_EQ( compareResult.numDifferentPixels, 0 ); + EXPECT_EQ( compareResult.maxDeltaE, 0.0 ); + EXPECT_TRUE( compareResult.diffImage == nullptr ); +} + +UTEST( ImageDiff, AlphaOnlyDifference ) { + const Uint8 pixels1[] = { 12, 34, 56, 78 }; + const Uint8 pixels2[] = { 12, 34, 56, 79 }; + const Image image1( pixels1, 1, 1, 4 ); + const Image image2( pixels2, 1, 1, 4 ); + + Image::DiffResult result = image1.diff( image2, 2.3f, Color( 255, 0, 255, 255 ), false ); + EXPECT_EQ( result.numDifferentPixels, 1 ); + EXPECT_EQ( result.maxDeltaE, 0.0 ); + EXPECT_TRUE( result.diffImage == nullptr ); +} + +UTEST( ImageDiff, RGBDifferenceThreshold ) { + const Uint8 pixels1[] = { 255, 0, 0 }; + const Uint8 pixels2[] = { 0, 0, 255 }; + const Image image1( pixels1, 1, 1, 3 ); + const Image image2( pixels2, 1, 1, 3 ); + + Image::DiffResult belowThreshold = + image1.diff( image2, 1000.f, Color( 255, 0, 255, 255 ), false ); + EXPECT_EQ( belowThreshold.numDifferentPixels, 0 ); + EXPECT_TRUE( belowThreshold.maxDeltaE > 0.0 ); + + Image::DiffResult aboveThreshold = image1.diff( image2, 0.f, Color( 255, 0, 255, 255 ), false ); + EXPECT_EQ( aboveThreshold.numDifferentPixels, 1 ); + EXPECT_EQ( aboveThreshold.maxDeltaE, belowThreshold.maxDeltaE ); +} + +UTEST( ImageDiff, DiffImageRendering ) { + const Uint8 pixels1[] = { 30, 60, 90, 255, 255, 0, 0, 255 }; + const Uint8 pixels2[] = { 30, 60, 90, 255, 0, 0, 255, 255 }; + const Color diffColor( 1, 2, 3, 4 ); + const Image image1( pixels1, 2, 1, 4 ); + const Image image2( pixels2, 2, 1, 4 ); + + Image::DiffResult result = image1.diff( image2, 0.f, diffColor, true ); + ASSERT_TRUE( result.diffImage != nullptr ); + EXPECT_EQ( result.numDifferentPixels, 1 ); + const Uint8* diffPixels = result.diffImage->getPixelsPtr(); + EXPECT_EQ( diffPixels[0], 60 ); + EXPECT_EQ( diffPixels[1], 60 ); + EXPECT_EQ( diffPixels[2], 60 ); + EXPECT_EQ( diffPixels[3], 128 ); + EXPECT_EQ( diffPixels[4], diffColor.r ); + EXPECT_EQ( diffPixels[5], diffColor.g ); + EXPECT_EQ( diffPixels[6], diffColor.b ); + EXPECT_EQ( diffPixels[7], diffColor.a ); +} + +UTEST( ImageDiff, DifferentDimensions ) { + const Image image1( 2, 3, 3 ); + const Image image2( 1, 1, 3 ); + + Image::DiffResult result = image1.diff( image2 ); + EXPECT_EQ( result.numDifferentPixels, 6 ); + EXPECT_EQ( result.maxDeltaE, 0.0 ); + EXPECT_TRUE( result.diffImage == nullptr ); +}