Skip to content

Commit 74ccf79

Browse files
authored
Add new imagediff tool (#22)
This change introduces a new image diff tool that has two modes for comparing PNG images. The "Exact Match" mode loads a PNG and compares the decoded image data. This comensates for differences in the image generation tool, image headers or other encoding differences. The "CIE76" mode compares the two images computing the CIE76 color difference, and fails if the difference of the worst pixel is greater than 2.3, which corresponds to a "just noticeable" difference. Additionally, when using the CIE match mode, a rules file may be specified with the -rules option to allow customizing the failure conditions based on furthest color distance, root mean square of the whole image, root mean square of the noticeably different pixels, percentage of pixels with differences, or percentage-based tolerance intervals. The tool can also generate an image that represents the difference between the two rendered images. In addition to introducing this tooling, this change also enables golden-image verification testing. Golden images are stored in a separate git repo (https://github.com/llvm-beanz/offload-golden-images), and are opted into during configuring the build by setting GOLDENIMAGE_DIR to the root of that checkout. The only image currently tested is the Mandelbrot test. A note for the future: CIE has several newer color difference models which would be worth using in place of the CIE76 model. I only implemented CIE76 because it was easy.
1 parent 80fe4c1 commit 74ccf79

File tree

14 files changed

+555
-28
lines changed

14 files changed

+555
-28
lines changed

.github/workflows/test-all.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,13 @@ jobs:
137137
ref: ${{ inputs.HLSLTest-branch }}
138138
path: HLSLTest
139139
fetch-depth: 1
140+
- name: Checkout Golden Images
141+
uses: actions/checkout@v4
142+
with:
143+
repository: llvm-beanz/offload-golden-images
144+
ref: main
145+
path: golden-images
146+
fetch-depth: 1
140147
- name: Setup Windows
141148
if: inputs.OS == 'windows'
142149
uses: llvm/actions/setup-windows@main
@@ -154,7 +161,7 @@ jobs:
154161
cd llvm-project
155162
mkdir build
156163
cd build
157-
cmake -G Ninja ${{ inputs.LLVM-ExtraCMakeArgs }} -DCMAKE_BUILD_TYPE=${{ inputs.BuildType }} -C ${{ github.workspace }}/llvm-project/clang/cmake/caches/HLSL.cmake -C ${{ github.workspace }}/HLSLTest/cmake/caches/sccache.cmake -DDXC_DIR=${{ github.workspace }}/DXC/build/bin -DLLVM_EXTERNAL_HLSLTEST_SOURCE_DIR=${{ github.workspace }}/HLSLTest -DLLVM_EXTERNAL_PROJECTS="HLSLTest" -DLLVM_LIT_ARGS="--xunit-xml-output=testresults.xunit.xml -v" -DHLSLTEST_TEST_CLANG=${{ inputs.Test-Clang }} ${{ github.workspace }}/llvm-project/llvm/
164+
cmake -G Ninja ${{ inputs.LLVM-ExtraCMakeArgs }} -DCMAKE_BUILD_TYPE=${{ inputs.BuildType }} -C ${{ github.workspace }}/llvm-project/clang/cmake/caches/HLSL.cmake -C ${{ github.workspace }}/HLSLTest/cmake/caches/sccache.cmake -DDXC_DIR=${{ github.workspace }}/DXC/build/bin -DLLVM_EXTERNAL_HLSLTEST_SOURCE_DIR=${{ github.workspace }}/HLSLTest -DLLVM_EXTERNAL_PROJECTS="HLSLTest" -DLLVM_LIT_ARGS="--xunit-xml-output=testresults.xunit.xml -v" -DHLSLTEST_TEST_CLANG=${{ inputs.Test-Clang }} -DGOLDENIMAGE_DIR=${{ github.workspace }}/golden-images ${{ github.workspace }}/llvm-project/llvm/
158165
ninja hlsl-test-depends
159166
- name: Run HLSL Tests
160167
run: |

include/Image/Color.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
#define HLSLTEST_IMAGE_COLOR_H
1414

1515
#include <algorithm>
16+
#include <assert.h>
17+
#include <math.h>
1618
#include <tuple>
1719
#include <type_traits>
1820

@@ -39,6 +41,7 @@ template <typename NewTy, typename OldTy> NewTy convertColor(OldTy Val) {
3941
if constexpr (std::is_same_v<NewTy, OldTy>)
4042
return Val;
4143
double Dbl = toDouble(Val);
44+
assert(Dbl >= 0.0 && Dbl <= 1.0 && "Value should be normalized");
4245
if constexpr (std::is_integral_v<NewTy>)
4346
return toInt<NewTy>(Dbl);
4447
return static_cast<NewTy>(Dbl);
@@ -90,6 +93,18 @@ class Color : public ColorBase<double> {
9093
return translateSpaceImpl(NewCS);
9194
}
9295

96+
Color operator-(const Color &C) const {
97+
assert(Space == C.Space && "Subtracting colors in different spaces!");
98+
return Color(abs(R - C.R), abs(G - C.G), abs(B - C.B), Space);
99+
}
100+
101+
static double CIE75Distance(Color L, Color R) {
102+
Color LCol = L.translateSpace(ColorSpace::LAB);
103+
Color RCol = R.translateSpace(ColorSpace::LAB);
104+
Color Res = LCol - RCol;
105+
return sqrt((Res.R * Res.R) + (Res.G * Res.G) + (Res.B * Res.B));
106+
}
107+
93108
private:
94109
Color translateSpaceImpl(ColorSpace NewCS);
95110
};

include/Image/Image.h

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,51 @@
1212
#ifndef HLSLTEST_IMAGE_IMAGE_H
1313
#define HLSLTEST_IMAGE_IMAGE_H
1414

15+
#include "Image/Color.h"
1516
#include "Support/Pipeline.h"
1617

18+
#include "llvm/ADT/ArrayRef.h"
1719
#include "llvm/ADT/StringRef.h"
1820
#include "llvm/ADT/bit.h"
1921
#include "llvm/Support/Error.h"
2022

2123
#include <cassert>
2224
#include <cstdint>
2325

26+
namespace llvm {
27+
class raw_ostream;
28+
}
2429
namespace hlsltest {
2530

31+
class ImageComparatorBase {
32+
public:
33+
virtual ~ImageComparatorBase() {}
34+
virtual void processPixel(Color L, Color R) = 0;
35+
virtual void print(llvm::raw_ostream &OS) {}
36+
virtual bool result() { return true; }
37+
};
38+
39+
class ImageComparatorRef {
40+
std::unique_ptr<ImageComparatorBase> Comp;
41+
ImageComparatorRef() = delete;
42+
43+
public:
44+
ImageComparatorRef(std::unique_ptr<ImageComparatorBase> &&C)
45+
: Comp(std::move(C)) {}
46+
ImageComparatorRef(ImageComparatorRef &&) = default;
47+
void processPixel(Color L, Color R) { Comp->processPixel(L, R); }
48+
49+
void print(llvm::raw_ostream &OS) { Comp->print(OS); };
50+
51+
bool result() { return Comp->result(); }
52+
};
53+
54+
template <typename T, typename... ArgTs>
55+
std::enable_if_t<std::is_base_of_v<ImageComparatorBase, T>, ImageComparatorRef>
56+
make_comparator(ArgTs &&...Args) {
57+
return ImageComparatorRef(std::make_unique<T>(Args...));
58+
}
59+
2660
class ImageRef {
2761
uint32_t Height;
2862
uint32_t Width;
@@ -81,6 +115,16 @@ class Image : public ImageRef {
81115
Data = llvm::StringRef(OwnedData.get(), Sz);
82116
}
83117

118+
Image(uint32_t H, uint32_t W, uint8_t D, uint8_t C, bool F,
119+
std::unique_ptr<char[]> &&Ptr)
120+
: ImageRef(H, W, D, C, F), OwnedData(std::move(Ptr)) {
121+
uint64_t Sz = static_cast<uint64_t>(H) * static_cast<uint64_t>(W) *
122+
static_cast<uint64_t>(D) * static_cast<uint64_t>(C);
123+
Data = llvm::StringRef(OwnedData.get(), Sz);
124+
}
125+
126+
friend class ImageComparatorDiffImage;
127+
84128
public:
85129
// Not default constructable.
86130
Image() = delete;
@@ -93,10 +137,15 @@ class Image : public ImageRef {
93137

94138
ImageRef getRef() const { return ImageRef(*this); }
95139

96-
static llvm::Error WritePNG(ImageRef I, llvm::StringRef Path);
140+
static llvm::Error writePNG(ImageRef I, llvm::StringRef Path);
97141

98142
static Image translateImage(ImageRef I, uint8_t Depth, uint8_t Channels,
99143
bool Float);
144+
static llvm::Expected<Image> loadPNG(llvm::StringRef Path);
145+
146+
static llvm::Error
147+
compareImages(ImageRef LHS, ImageRef RHS,
148+
llvm::MutableArrayRef<ImageComparatorRef> Comparators);
100149

101150
char *data() { return OwnedData.get(); }
102151
};

include/Image/ImageComparators.h

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
//===- ImageComparators.h - Image Comparators -------------------*- C++ -*-===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
//
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
#include "Image/Color.h"
13+
#include "Image/Image.h"
14+
15+
#include "llvm/Support/YAMLTraits.h"
16+
#include "llvm/Support/raw_ostream.h"
17+
18+
#include <algorithm>
19+
20+
#ifndef HLSLTEST_IMAGE_IMAGECOMPARATOR_H
21+
#define HLSLTEST_IMAGE_IMAGECOMPARATOR_H
22+
23+
namespace hlsltest {
24+
25+
struct CompareCheck {
26+
enum CheckType {
27+
None,
28+
Furthest,
29+
RMS,
30+
DiffRMS,
31+
PixelPercent,
32+
Intervals,
33+
} Type = None;
34+
double Val = 0;
35+
std::vector<double> Vals = {};
36+
};
37+
38+
class ImageComparatorDistance : public ImageComparatorBase {
39+
double Furthest = 0.0;
40+
double RMS = 0.0;
41+
double DiffRMS = 0.0;
42+
uint64_t Count = 0;
43+
uint64_t VisibleDiffs = 0;
44+
uint64_t Histogram[10] = {0};
45+
46+
llvm::SmallVector<CompareCheck> Checks;
47+
48+
llvm::SmallString<256> ErrStr;
49+
50+
// 2.3 corresponds to a "just noticeable distance" for the CIE76
51+
// difference algorithm. If no pixels are worse than that, there should be
52+
// no noticeable difference in the image.
53+
static constexpr double VisibleDiff = 2.3;
54+
55+
public:
56+
ImageComparatorDistance() {
57+
Checks.push_back(CompareCheck{CompareCheck::Furthest, VisibleDiff});
58+
}
59+
ImageComparatorDistance(llvm::ArrayRef<CompareCheck> C) : Checks(C) {}
60+
void processPixel(Color L, Color R) override {
61+
double Distance = Color::CIE75Distance(L, R);
62+
if (Distance > VisibleDiff) {
63+
VisibleDiffs += 1;
64+
DiffRMS += Distance;
65+
66+
// A difference >10 is a more than 10% off in the L*a*b color space.
67+
int Idx = static_cast<int>(std::clamp(Distance - VisibleDiff, 0.0, 9.0));
68+
Histogram[Idx] += 1;
69+
}
70+
71+
Furthest = std::max(Furthest, Distance);
72+
RMS += Distance;
73+
Count += 1;
74+
}
75+
76+
void print(llvm::raw_ostream &OS) override {
77+
double CountDbl = static_cast<double>(Count);
78+
OS << "RMS Difference: " << RMS << "\n";
79+
OS << "Furthest Pixel Difference: " << Furthest << "\n";
80+
OS << "Pixels with visible differences: " << VisibleDiffs << " "
81+
<< (static_cast<double>(VisibleDiffs) / CountDbl * 100.0) << "%\n";
82+
OS << "RMS Different Pixels Only: " << DiffRMS << "\n";
83+
OS << "Total Pixels: " << Count << "\n";
84+
OS << "Histogram Data:\n";
85+
for (int I = 0; I < 10; ++I) {
86+
OS << "\t[" << I << "]: " << Histogram[I] << " "
87+
<< (static_cast<double>(Histogram[I]) / CountDbl * 100.0) << "\n";
88+
}
89+
if (!ErrStr.empty())
90+
OS << "Error: " << ErrStr << "\n";
91+
}
92+
93+
bool evaluateCheck(const CompareCheck &C, llvm::raw_ostream &Err);
94+
95+
bool result() override {
96+
RMS = sqrt(RMS / static_cast<double>(Count));
97+
DiffRMS = sqrt(DiffRMS / static_cast<double>(VisibleDiffs));
98+
llvm::raw_svector_ostream Err(ErrStr);
99+
100+
for (const auto &C : Checks) {
101+
if (!evaluateCheck(C, Err))
102+
return false;
103+
}
104+
105+
return true;
106+
}
107+
};
108+
109+
class ImageComparatorDiffImage : public ImageComparatorBase {
110+
Image DiffImg;
111+
float *DiffPtr;
112+
llvm::StringRef OutputFilename;
113+
114+
public:
115+
virtual ~ImageComparatorDiffImage() {}
116+
ImageComparatorDiffImage(uint32_t Height, uint32_t Width, llvm::StringRef OF)
117+
: DiffImg(Height, Width, 4, 3, true), OutputFilename(OF) {
118+
DiffPtr = reinterpret_cast<float *>(DiffImg.data());
119+
}
120+
void processPixel(Color L, Color R) override {
121+
// TODO: I should probably instead use a color distance for this too...
122+
DiffPtr[0] = abs(L.R - R.R);
123+
DiffPtr[1] = abs(L.G - R.G);
124+
DiffPtr[2] = abs(L.B - R.B);
125+
DiffPtr += 3;
126+
}
127+
128+
void print(llvm::raw_ostream &) override {
129+
llvm::consumeError(Image::writePNG(DiffImg, OutputFilename));
130+
}
131+
};
132+
} // namespace hlsltest
133+
134+
LLVM_YAML_IS_SEQUENCE_VECTOR(hlsltest::CompareCheck)
135+
136+
namespace llvm {
137+
namespace yaml {
138+
139+
template <> struct MappingTraits<hlsltest::CompareCheck> {
140+
static void mapping(IO &I, hlsltest::CompareCheck &C);
141+
};
142+
143+
template <> struct ScalarEnumerationTraits<hlsltest::CompareCheck::CheckType> {
144+
static void enumeration(IO &I, hlsltest::CompareCheck::CheckType &V) {
145+
#define ENUM_CASE(Val) I.enumCase(V, #Val, hlsltest::CompareCheck::Val)
146+
ENUM_CASE(Furthest);
147+
ENUM_CASE(RMS);
148+
ENUM_CASE(DiffRMS);
149+
ENUM_CASE(PixelPercent);
150+
ENUM_CASE(Intervals);
151+
#undef ENUM_CASE
152+
}
153+
};
154+
} // namespace yaml
155+
} // namespace llvm
156+
157+
#endif // HLSLTEST_IMAGE_IMAGECOMPARATOR_H

lib/Image/CMakeLists.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
add_hlsl_library(Image Color.cpp Image.cpp)
1+
add_hlsl_library(Image
2+
Color.cpp
3+
Image.cpp
4+
ImageComparators.cpp)
25

36
target_include_directories(HLSLTestImage PRIVATE SYSTEM BEFORE
47
"${HLSLTEST_BINARY_DIR}/third-party/libpng/"

0 commit comments

Comments
 (0)