506 lines
13 KiB
C++
506 lines
13 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#pragma once
|
|
|
|
#include "Containers/ContainersFwd.h"
|
|
#include "Containers/UnrealString.h"
|
|
#include "CoreMinimal.h"
|
|
#include "Internationalization/Text.h"
|
|
#include "Math/Color.h"
|
|
#include "Math/UnrealMathSSE.h"
|
|
#include "Misc/AssertionMacros.h"
|
|
#include "Misc/DateTime.h"
|
|
#include "Misc/Paths.h"
|
|
#include "Templates/SharedPointer.h"
|
|
#include "UObject/ObjectMacros.h"
|
|
|
|
#include "ImageComparer.generated.h"
|
|
|
|
#define UE_API SCREENSHOTCOMPARISONTOOLS_API
|
|
|
|
class Error;
|
|
class FComparableImage;
|
|
|
|
/**
|
|
*
|
|
*/
|
|
USTRUCT()
|
|
struct FImageTolerance
|
|
{
|
|
GENERATED_USTRUCT_BODY()
|
|
|
|
public:
|
|
|
|
UPROPERTY(EditAnywhere, Category = Automation)
|
|
uint8 Red;
|
|
|
|
UPROPERTY(EditAnywhere, Category = Automation)
|
|
uint8 Green;
|
|
|
|
UPROPERTY(EditAnywhere, Category = Automation)
|
|
uint8 Blue;
|
|
|
|
UPROPERTY(EditAnywhere, Category = Automation)
|
|
uint8 Alpha;
|
|
|
|
UPROPERTY(EditAnywhere, Category = Automation)
|
|
uint8 MinBrightness;
|
|
|
|
UPROPERTY(EditAnywhere, Category = Automation)
|
|
uint8 MaxBrightness;
|
|
|
|
UPROPERTY(EditAnywhere, Category = Automation)
|
|
bool IgnoreAntiAliasing;
|
|
|
|
UPROPERTY(EditAnywhere, Category = Automation)
|
|
bool IgnoreColors;
|
|
|
|
UPROPERTY(EditAnywhere, Category = Automation)
|
|
float MaximumLocalError;
|
|
|
|
UPROPERTY(EditAnywhere, Category = Automation)
|
|
float MaximumGlobalError;
|
|
|
|
FImageTolerance()
|
|
: Red(0)
|
|
, Green(0)
|
|
, Blue(0)
|
|
, Alpha(0)
|
|
, MinBrightness(0)
|
|
, MaxBrightness(255)
|
|
, IgnoreAntiAliasing(false)
|
|
, IgnoreColors(false)
|
|
, MaximumLocalError(0.0f)
|
|
, MaximumGlobalError(0.0f)
|
|
{
|
|
}
|
|
|
|
FImageTolerance(uint8 R, uint8 G, uint8 B, uint8 A, uint8 InMinBrightness, uint8 InMaxBrightness, bool InIgnoreAntiAliasing, bool InIgnoreColors, float InMaximumLocalError, float InMaximumGlobalError)
|
|
: Red(R)
|
|
, Green(G)
|
|
, Blue(B)
|
|
, Alpha(A)
|
|
, MinBrightness(InMinBrightness)
|
|
, MaxBrightness(InMaxBrightness)
|
|
, IgnoreAntiAliasing(InIgnoreAntiAliasing)
|
|
, IgnoreColors(InIgnoreColors)
|
|
, MaximumLocalError(InMaximumLocalError)
|
|
, MaximumGlobalError(InMaximumGlobalError)
|
|
{
|
|
}
|
|
|
|
public:
|
|
UE_API const static FImageTolerance DefaultIgnoreNothing;
|
|
UE_API const static FImageTolerance DefaultIgnoreLess;
|
|
UE_API const static FImageTolerance DefaultIgnoreAntiAliasing;
|
|
UE_API const static FImageTolerance DefaultIgnoreColors;
|
|
};
|
|
|
|
UENUM()
|
|
enum class EImageTolerancePreset : uint8
|
|
{
|
|
IgnoreNothing,
|
|
IgnoreLess,
|
|
IgnoreAntiAliasing,
|
|
IgnoreColors,
|
|
Custom
|
|
};
|
|
|
|
static FImageTolerance GetImageToleranceForPreset(EImageTolerancePreset Preset, FImageTolerance CustomTolerance)
|
|
{
|
|
switch (Preset)
|
|
{
|
|
case EImageTolerancePreset::IgnoreNothing: return FImageTolerance::DefaultIgnoreNothing;
|
|
case EImageTolerancePreset::IgnoreLess: return FImageTolerance::DefaultIgnoreLess;
|
|
case EImageTolerancePreset::IgnoreAntiAliasing: return FImageTolerance::DefaultIgnoreAntiAliasing;
|
|
case EImageTolerancePreset::IgnoreColors: return FImageTolerance::DefaultIgnoreColors;
|
|
case EImageTolerancePreset::Custom: return CustomTolerance;
|
|
default:
|
|
check(0);
|
|
}
|
|
|
|
return FImageTolerance();
|
|
}
|
|
|
|
class FComparableImage;
|
|
|
|
class FPixelOperations
|
|
{
|
|
public:
|
|
static FORCEINLINE double GetLuminance(const FColor& Color)
|
|
{
|
|
// https://en.wikipedia.org/wiki/Relative_luminance
|
|
return (0.2126 * Color.R + 0.7152 * Color.G + 0.0722 * Color.B) * (Color.A / 255.0);
|
|
}
|
|
|
|
static bool IsBrightnessSimilar(const FColor& ColorA, const FColor& ColorB, const FImageTolerance& Tolerance)
|
|
{
|
|
const bool AlphaSimilar = FMath::IsNearlyEqual((float)ColorA.A, ColorB.A, Tolerance.Alpha);
|
|
|
|
const double BrightnessA = FPixelOperations::GetLuminance(ColorA);
|
|
const double BrightnessB = FPixelOperations::GetLuminance(ColorB);
|
|
const bool BrightnessSimilar = FMath::IsNearlyEqual(BrightnessA, BrightnessB, Tolerance.MinBrightness);
|
|
|
|
return BrightnessSimilar && AlphaSimilar;
|
|
}
|
|
|
|
static FORCEINLINE bool IsRGBSame(const FColor& ColorA, const FColor& ColorB)
|
|
{
|
|
return ColorA.R == ColorB.R &&
|
|
ColorA.G == ColorB.G &&
|
|
ColorA.B == ColorB.B;
|
|
}
|
|
|
|
static FORCEINLINE bool IsRGBSimilar(const FColor& ColorA, const FColor& ColorB, const FImageTolerance& Tolerance)
|
|
{
|
|
const bool RedSimilar = FMath::IsNearlyEqual((float)ColorA.R, ColorB.R, Tolerance.Red);
|
|
const bool GreenSimilar = FMath::IsNearlyEqual((float)ColorA.G, ColorB.G, Tolerance.Green);
|
|
const bool BlueSimilar = FMath::IsNearlyEqual((float)ColorA.B, ColorB.B, Tolerance.Blue);
|
|
const bool AlphaSimilar = FMath::IsNearlyEqual((float)ColorA.A, ColorB.A, Tolerance.Alpha);
|
|
|
|
return RedSimilar && GreenSimilar && BlueSimilar && AlphaSimilar;
|
|
}
|
|
|
|
static FORCEINLINE bool IsContrasting(const FColor& ColorA, const FColor& ColorB, const FImageTolerance& Tolerance)
|
|
{
|
|
const double BrightnessA = FPixelOperations::GetLuminance(ColorA);
|
|
const double BrightnessB = FPixelOperations::GetLuminance(ColorB);
|
|
|
|
return FMath::Abs(BrightnessA - BrightnessB) > Tolerance.MaxBrightness;
|
|
}
|
|
|
|
static float GetHue(const FColor& Color);
|
|
|
|
static bool IsAntialiased(const FColor& SourcePixel, const FComparableImage* Image, int32 X, int32 Y, const FImageTolerance& Tolerance);
|
|
};
|
|
|
|
/**
|
|
*
|
|
*/
|
|
class FComparableImage
|
|
{
|
|
public:
|
|
int32 Width = 0;
|
|
int32 Height = 0;
|
|
TArray64<uint8> Bytes;
|
|
|
|
FComparableImage()
|
|
{
|
|
}
|
|
|
|
FORCEINLINE bool CanGetPixel(int32 X, int32 Y) const
|
|
{
|
|
return X >= 0 && Y >= 0 && X < Width && Y < Height;
|
|
}
|
|
|
|
FORCEINLINE FColor GetPixel(int32 X, int32 Y) const
|
|
{
|
|
int64 Offset = ( (int64)Y * Width + X ) * 4;
|
|
check(Offset < ( (int64)Width * Height * 4 ));
|
|
|
|
return FColor(
|
|
Bytes[Offset],
|
|
Bytes[Offset + 1],
|
|
Bytes[Offset + 2],
|
|
Bytes[Offset + 3]);
|
|
}
|
|
|
|
/**
|
|
* Populate image by loading an file
|
|
*
|
|
* @param ImagePath Path for the image file to load
|
|
* @param OutError Contains the error message if load fails
|
|
*
|
|
* @return true if success
|
|
*/
|
|
UE_API bool LoadFile(const FString& ImagePath, FText& OutError);
|
|
|
|
/**
|
|
* Populate image by loading compressed data
|
|
*
|
|
* @param CompressedData The memory address of the start of the compressed data.
|
|
* @param CompressedSize The size of the compressed data parsed.
|
|
* @param ImageExtension File extension of the image format
|
|
* @param OutError Contains the error message if load fails
|
|
*
|
|
* @return true if success
|
|
*/
|
|
UE_API bool LoadCompressedData(const void* CompressedData, int64 CompressedSize, const FString& ImageExtension, FText& OutError);
|
|
};
|
|
|
|
/**
|
|
* This struct holds the results of comparing an incoming image from a platform with an approved image that exists under the
|
|
* project hierarchy.
|
|
|
|
* All paths in this structure should be portable. Test results (including this struct) result can be serialized to
|
|
* JSON and stored on the network as during automation runs then opened in the editor to commit / approve changes
|
|
* to the local project.
|
|
*/
|
|
USTRUCT()
|
|
struct FImageComparisonResult
|
|
{
|
|
GENERATED_USTRUCT_BODY()
|
|
|
|
public:
|
|
|
|
/*
|
|
Time that the comparison was performed
|
|
*/
|
|
UPROPERTY()
|
|
FDateTime CreationTime;
|
|
|
|
/*
|
|
Platform that the incoming image was generated on
|
|
*/
|
|
UPROPERTY()
|
|
FString SourcePlatform;
|
|
|
|
/*
|
|
RHI that the incoming image was generated with
|
|
*/
|
|
UPROPERTY()
|
|
FString SourceRHI;
|
|
|
|
/*
|
|
Path to a folder where the idealized ground-truth for this comparison would be. Relative to the project directory.
|
|
Note: This path may not exist a fallback is being used for approval, or if there is no approved
|
|
image at all. Comparing this value with the FPaths::GetPath(ApprovedFilePath) can be used to determine that.
|
|
(the IsIdeal() function performs that check).
|
|
*/
|
|
UPROPERTY()
|
|
FString IdealApprovedFolderPath;
|
|
|
|
/*
|
|
Path to the file that was considered as the ground-truth. Relative to the project directory
|
|
*/
|
|
UPROPERTY()
|
|
FString ApprovedFilePath;
|
|
|
|
/*
|
|
Path to the file that was generated in the test. Relative to the project directory, only valid when a test is run locally
|
|
*/
|
|
UPROPERTY()
|
|
FString IncomingFilePath;
|
|
|
|
/*
|
|
Path to the delta image between the ground-truth and the incoming file. Relative to the project directory, only valid when a test is run locally
|
|
*/
|
|
UPROPERTY()
|
|
FString ComparisonFilePath;
|
|
|
|
/*
|
|
Name of the approved file saved for the report. Path is relative to the location of the metadata for the report
|
|
*/
|
|
UPROPERTY()
|
|
FString ReportApprovedFilePath;
|
|
|
|
/*
|
|
name of the incoming file saved for the report. Path is relative to the location of the metadata for the report
|
|
*/
|
|
UPROPERTY()
|
|
FString ReportIncomingFilePath;
|
|
|
|
/*
|
|
Name of the delta image saved for the report. Path is relative to the location of the metadata for the report
|
|
*/
|
|
UPROPERTY()
|
|
FString ReportComparisonFilePath;
|
|
|
|
/*
|
|
Largest local difference found during comparison
|
|
*/
|
|
UPROPERTY()
|
|
double MaxLocalDifference;
|
|
|
|
/*
|
|
Global difference found during comparison
|
|
*/
|
|
UPROPERTY()
|
|
double GlobalDifference;
|
|
|
|
/*
|
|
Tolerance values for comparison
|
|
*/
|
|
UPROPERTY()
|
|
FImageTolerance Tolerance;
|
|
|
|
/*
|
|
Error message that can be set during a comparison
|
|
*/
|
|
UPROPERTY()
|
|
FText ErrorMessage;
|
|
|
|
/*
|
|
Path of the screenshot (includes variant if applicable)
|
|
*/
|
|
UPROPERTY()
|
|
FString ScreenshotPath;
|
|
|
|
/*
|
|
Id of the comparison request
|
|
*/
|
|
UPROPERTY()
|
|
FGuid ComparisonId;
|
|
|
|
/*
|
|
Whether to skip saving and attaching images to the report for this test
|
|
*/
|
|
UPROPERTY()
|
|
bool bSkipAttachingImages;
|
|
|
|
/*
|
|
Version of the image comparision result
|
|
*/
|
|
UPROPERTY()
|
|
int32 Version;
|
|
|
|
static constexpr int32 CurrentVersion = 3;
|
|
static constexpr int32 OldestSupportedVersion = 2;
|
|
|
|
FImageComparisonResult()
|
|
: CreationTime(0)
|
|
, MaxLocalDifference(0.0f)
|
|
, GlobalDifference(0.0f)
|
|
, ErrorMessage()
|
|
, bSkipAttachingImages(false)
|
|
, Version(CurrentVersion)
|
|
{
|
|
}
|
|
|
|
FImageComparisonResult(const FText& Error)
|
|
: CreationTime(0)
|
|
, MaxLocalDifference(0.0f)
|
|
, GlobalDifference(0.0f)
|
|
, ErrorMessage(Error)
|
|
, bSkipAttachingImages(false)
|
|
, Version(CurrentVersion)
|
|
{
|
|
}
|
|
|
|
/*
|
|
Returns true if this is a new image with no approved file to compare against
|
|
*/
|
|
bool IsValid() const
|
|
{
|
|
return Version >= OldestSupportedVersion && Version <= CurrentVersion;
|
|
}
|
|
|
|
/*
|
|
Marks this struct as invalid. Can be used before serializing in to ensure very old files with
|
|
no version info aren't recognized.
|
|
*/
|
|
void SetInvalid()
|
|
{
|
|
Version = 0;
|
|
}
|
|
|
|
/*
|
|
Returns true if this is a new image with no approved file to compare against
|
|
*/
|
|
bool IsNew() const
|
|
{
|
|
return ApprovedFilePath.IsEmpty();
|
|
}
|
|
|
|
/*
|
|
Returns true if this is am ideal comparison (e.g not using a fallback for
|
|
comparison)
|
|
*/
|
|
bool IsIdeal() const
|
|
{
|
|
return FPaths::GetPath(ApprovedFilePath) == IdealApprovedFolderPath;
|
|
}
|
|
|
|
/*
|
|
Returns true if the images were within the provided tolerance values
|
|
*/
|
|
bool AreSimilar() const
|
|
{
|
|
if ( IsNew() )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!ErrorMessage.IsEmpty())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ( MaxLocalDifference > Tolerance.MaximumLocalError || GlobalDifference > Tolerance.MaximumGlobalError )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
};
|
|
|
|
struct FComparisonReport
|
|
{
|
|
public:
|
|
|
|
/*
|
|
ReportRootDirectory is where all reports are saved. E.g. <path>/Saved/Automation/Reports
|
|
ReportFile is a specific report for a test under this path. E.g. <path>/Saved/Automation/Reports/Test/TestName/report.json.
|
|
*/
|
|
|
|
UE_API FComparisonReport(const FString& InReportRootDirectory, const FString& InReportFile);
|
|
|
|
void SetComparisonResult(const FImageComparisonResult& InResult)
|
|
{
|
|
Comparison = InResult;
|
|
}
|
|
|
|
const FImageComparisonResult& GetComparisonResult() const
|
|
{
|
|
return Comparison;
|
|
}
|
|
|
|
/*
|
|
Return the path to the file used to generate this report
|
|
*/
|
|
const FString& GetReportFile() const { return ReportFile; }
|
|
|
|
/*
|
|
Return the path to all files in this report
|
|
*/
|
|
const FString& GetReportPath() const { return ReportPath; }
|
|
|
|
/*
|
|
Return the path to a location where all reports (including this one) are kept in this session
|
|
*/
|
|
const FString& GetReportRootDirectory() const { return ReportRootDirectory; }
|
|
|
|
private:
|
|
|
|
FString ReportRootDirectory;
|
|
FString ReportFile;
|
|
FString ReportPath;
|
|
FImageComparisonResult Comparison;
|
|
};
|
|
|
|
/**
|
|
*
|
|
*/
|
|
class FImageComparer
|
|
{
|
|
public:
|
|
|
|
UE_API FImageComparisonResult Compare(const FString& ImagePathA, const FString& ImagePathB, FImageTolerance Tolerance, const FString& OutDeltaPath);
|
|
UE_API FImageComparisonResult Compare(const FComparableImage* ImageA, const FComparableImage* ImageB, FImageTolerance Tolerance, const FString& OutDeltaPath);
|
|
|
|
enum class EStructuralSimilarityComponent : uint8
|
|
{
|
|
Luminance,
|
|
Color
|
|
};
|
|
|
|
/**
|
|
* https://en.wikipedia.org/wiki/Structural_similarity
|
|
*/
|
|
UE_API double CompareStructuralSimilarity(const FString& ImagePathA, const FString& ImagePathB, EStructuralSimilarityComponent InCompareComponent, const FString& OutDeltaPath);
|
|
UE_API double CompareStructuralSimilarity(const FComparableImage* ImageA, const FComparableImage* ImageB, EStructuralSimilarityComponent InCompareComponent, const FString& OutDeltaPath);
|
|
};
|
|
|
|
#undef UE_API
|