mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-09-22 13:01:05 +03:00
Implemented the Image::diff() optimization.
- Added exact RGB equality and alpha-only fast paths. - Added optional createDiffImage, defaulting to true. - Updated screenshot comparisons to compare-only first and generate visual output only on failure. - Flattened iteration into a pointer-walking loop. - Added a 256-entry sRGB-to-linear lookup table. - Preserved CIEDE2000, thresholds, alpha comparison, maxDeltaE, and visual rendering semantics. - Added six focused correctness tests in src/tests/unit_tests/image_diff_tests.cpp.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <eepp/system/packregistry.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
|
||||
#include <imageresampler/resampler.h>
|
||||
@@ -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<double, 256>& getSRGBToLinearLUT() {
|
||||
static const std::array<double, 256> lut = [] {
|
||||
std::array<double, 256> values{};
|
||||
for ( size_t i = 0; i < values.size(); ++i ) {
|
||||
const double c = static_cast<double>( 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<std::vector<Image>, 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<size_t>( getWidth() ) * static_cast<size_t>( 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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
97
src/tests/unit_tests/image_diff_tests.cpp
Normal file
97
src/tests/unit_tests/image_diff_tests.cpp
Normal file
@@ -0,0 +1,97 @@
|
||||
#include "utest.h"
|
||||
|
||||
#include <eepp/graphics/image.hpp>
|
||||
|
||||
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 );
|
||||
}
|
||||
Reference in New Issue
Block a user