Skip to content

Commit 113bde8

Browse files
authored
Add color utility (#20)
This PR adds a new color utility object for working with colors and transforming color spaces. It currently supports translating between RGB, CIEXYZ and CIELAB. The D65 white point is hard coded, but could be refactored to be configurable. This change also adds unit testing for the Image library so that this can be tested more cleanly, and adds the unit test target to the test-all build configuration.
1 parent 8ed70da commit 113bde8

File tree

10 files changed

+291
-3
lines changed

10 files changed

+291
-3
lines changed

.github/workflows/test-all.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ jobs:
160160
run: |
161161
cd llvm-project
162162
cd build
163+
ninja check-hlsl-unit
163164
ninja ${{ inputs.TestTarget }}
164165
- name: Publish Test Results
165166
uses: EnricoMi/publish-unit-test-result-action/macos@v2

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,5 @@ include(Warp)
9494
add_subdirectory(lib)
9595
add_subdirectory(tools)
9696

97+
add_subdirectory(unittests)
9798
add_subdirectory(test)

include/Image/Color.h

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//===- Color.h - Color Description ------------------------------*- 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+
#ifndef HLSLTEST_IMAGE_COLOR_H
13+
#define HLSLTEST_IMAGE_COLOR_H
14+
15+
namespace hlsltest {
16+
17+
class Color {
18+
public:
19+
enum Space {
20+
RGB,
21+
XYZ,
22+
LAB
23+
};
24+
25+
double R, G, B;
26+
27+
Space ColorSpace;
28+
29+
constexpr Color(double Rx, double Gy, double Bz, Space CS = RGB)
30+
: R(Rx), G(Gy), B(Bz), ColorSpace(CS) {}
31+
Color() = delete;
32+
Color(const Color &) = default;
33+
Color(Color &&) = default;
34+
Color &operator=(const Color &) = default;
35+
Color &operator=(Color &&) = default;
36+
37+
Color translateSpace(Space NewCS) {
38+
if (NewCS == ColorSpace)
39+
return *this;
40+
return translateSpaceImpl(NewCS);
41+
}
42+
43+
private:
44+
Color translateSpaceImpl(Space NewCS);
45+
};
46+
47+
} // namespace hlsltest
48+
49+
#endif // HLSLTEST_IMAGE_COLOR_H

lib/Image/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
add_hlsl_library(Image Image.cpp)
1+
add_hlsl_library(Image Color.cpp Image.cpp)
22

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

lib/Image/Color.cpp

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//===- Color.cpp - Color Description ----------------------------*- 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+
14+
#include <math.h>
15+
16+
using namespace hlsltest;
17+
18+
constexpr Color D65WhitePoint = Color(95.047, 100.000, 108.883, Color::XYZ);
19+
20+
static Color multiply(const Color LHS, double Mat[9], Color::Space NewSpace) {
21+
double X, Y, Z;
22+
X = (LHS.R * Mat[0]) + (LHS.G * Mat[1]) + (LHS.B * Mat[2]);
23+
Y = (LHS.R * Mat[3]) + (LHS.G * Mat[4]) + (LHS.B * Mat[5]);
24+
Z = (LHS.R * Mat[6]) + (LHS.G * Mat[7]) + (LHS.B * Mat[8]);
25+
return Color(X, Y, Z, NewSpace);
26+
}
27+
28+
static Color RGBToXYZ(const Color Old) {
29+
// Matrix assumes D65 white point.
30+
// Source: http://brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
31+
double Mat[] = {0.4124564, 0.3575761, 0.1804375, 0.2126729, 0.7151522,
32+
0.0721750, 0.0193339, 0.1191920, 0.9503041};
33+
return multiply(Old, Mat, Color::XYZ);
34+
}
35+
36+
static Color XYZToRGB(const Color Old) {
37+
// Matrix assumes D65 white point.
38+
// Source: http://brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
39+
double Mat[] = {3.2404542, -1.5371385, -0.4985314, -0.9692660, 1.8760108,
40+
0.0415560, 0.0556434, -0.2040259, 1.0572252};
41+
return multiply(Old, Mat, Color::RGB);
42+
}
43+
44+
static double convertXYZ(double Val) {
45+
constexpr double E = 216.0 / 24389.0;
46+
constexpr double K = 24389.0 / 27.0;
47+
return Val > E ? pow(Val, 1.0 / 3.0) : (K * Val + 16.0) / 116.0;
48+
}
49+
50+
static Color XYZToLAB(const Color Old) {
51+
double X = convertXYZ(Old.R / D65WhitePoint.R);
52+
double Y = convertXYZ(Old.G / D65WhitePoint.G);
53+
double Z = convertXYZ(Old.B / D65WhitePoint.B);
54+
55+
double L = fmax(0.0, 116.0 * Y - 16.0);
56+
double A = 500 * (X - Y);
57+
double B = 200 * (Y - Z);
58+
return Color(L, A, B, Color::LAB);
59+
}
60+
61+
static double convertLAB(double Val) {
62+
double ValPow3 = pow(Val, 3.0);
63+
if (ValPow3 > 0.008856)
64+
return ValPow3;
65+
66+
constexpr double V = 16.0 / 116.0;
67+
constexpr double K = (24389.0 / 27.0) / 116.0;
68+
return (Val - V) / K;
69+
}
70+
71+
static Color LABToXYZ(const Color Old) {
72+
double Y = (Old.R + 16) / 116;
73+
double X = Old.G / 500 + Y;
74+
double Z = Y - Old.B / 200;
75+
76+
X = convertLAB(X) * D65WhitePoint.R;
77+
Y = convertLAB(Y) * D65WhitePoint.G;
78+
Z = convertLAB(Z) * D65WhitePoint.B;
79+
80+
return Color(X, Y, Z, Color::XYZ);
81+
}
82+
83+
Color Color::translateSpaceImpl(Space NewCS) {
84+
Color Tmp = *this;
85+
if (ColorSpace == Color::RGB)
86+
Tmp = RGBToXYZ(*this);
87+
else if (ColorSpace == Color::LAB)
88+
Tmp = LABToXYZ(*this);
89+
// Tmp is now in XYZ space.
90+
if (NewCS == Color::RGB)
91+
return XYZToRGB(Tmp);
92+
if (NewCS == Color::LAB)
93+
return XYZToLAB(Tmp);
94+
return Tmp;
95+
}

test/CMakeLists.txt

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ list(APPEND HLSLTEST_DEPS
5858
api-query
5959
offloader
6060
FileCheck
61-
split-file)
61+
split-file
62+
HLSLTestUnit)
6263

6364
if (HLSLTEST_TEST_CLANG)
6465
list(APPEND HLSLTEST_DEPS clang)
@@ -117,7 +118,7 @@ function(add_hlsltest_lit_suite suite)
117118
"LLVM_TOOLS_DIR"
118119
)
119120

120-
add_lit_testsuite(check-hlsl-${suite}
121+
add_lit_testsuite(check-hlsl-${suite}
121122
"Running the HLSL regression tests for ${suite}"
122123
${CMAKE_CURRENT_BINARY_DIR}/${suite}
123124
DEPENDS ${HLSLTEST_DEPS}
@@ -132,6 +133,23 @@ endfunction()
132133

133134
umbrella_lit_testsuite_begin(check-hlsl)
134135

136+
# Add unit test suite
137+
configure_lit_site_cfg(
138+
${CMAKE_CURRENT_SOURCE_DIR}/Unit/lit.site.cfg.py.in
139+
${CMAKE_CURRENT_BINARY_DIR}/Unit/lit.site.cfg.py
140+
MAIN_CONFIG
141+
${CMAKE_CURRENT_BINARY_DIR}/Unit/lit.cfg.py
142+
PATHS
143+
"HLSLTEST_BINARY_DIR"
144+
"LLVM_TOOLS_DIR"
145+
)
146+
add_lit_testsuite(check-hlsl-unit
147+
"Running the offload test unit test suite"
148+
${CMAKE_CURRENT_BINARY_DIR}/Unit
149+
DEPENDS ${HLSLTEST_DEPS}
150+
)
151+
set_target_properties(check-hlsl-unit PROPERTIES FOLDER "HLSL tests")
152+
135153
if (NOT HLSLTEST_WARP_ONLY)
136154
foreach(platform ${platforms_to_test})
137155
set(TEST_${platform} True)

test/Unit/lit.site.cfg.py.in

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
@LIT_SITE_CFG_IN_HEADER@
2+
3+
import sys
4+
import os
5+
import subprocess
6+
7+
import lit.formats
8+
9+
config.hlsltest_obj_root = path(r"@HLSLTEST_BINARY_DIR@")
10+
config.hlsltest_src_root = path(r"@HLSLTEST_SOURCE_DIR@")
11+
config.llvm_build_mode = lit_config.substitute("@LLVM_BUILD_MODE@")
12+
config.gtest_run_under = lit_config.substitute(r"@LLVM_GTEST_RUN_UNDER@")
13+
14+
# name: The name of this test suite.
15+
config.name = "OffloadTest-Unit"
16+
17+
# suffixes: A list of file extensions to treat as test files.
18+
config.suffixes = []
19+
20+
# test_source_root: The root path where tests are located.
21+
# test_exec_root: The root path where tests should be run.
22+
config.test_exec_root = os.path.join(config.hlsltest_obj_root, "unittests")
23+
config.test_source_root = config.test_exec_root
24+
25+
# testFormat: The test format to use to interpret tests.
26+
config.test_format = lit.formats.GoogleTest(
27+
config.llvm_build_mode,
28+
"Tests",
29+
run_under=config.gtest_run_under,
30+
)
31+

unittests/CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
add_custom_target(HLSLTestUnit)
2+
3+
function(add_hlsltest_unittest test_dirname)
4+
add_unittest(HLSLTestUnit ${test_dirname} ${ARGN})
5+
endfunction()
6+
7+
add_subdirectory(Image)

unittests/Image/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
add_hlsltest_unittest(ImageTests ColorTests.cpp)
2+
3+
target_link_libraries(ImageTests PRIVATE HLSLTestImage)

unittests/Image/ColorTests.cpp

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//===- ColorTests.cpp - Color Tests -----------------------------*- 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+
14+
#include "llvm/Support/raw_ostream.h"
15+
16+
#include "gtest/gtest.h"
17+
18+
#include <algorithm>
19+
20+
using namespace hlsltest;
21+
22+
template <typename IntegerTy, typename FloatTy>
23+
IntegerTy NormalizeToInteger(FloatTy &Val) {
24+
constexpr FloatTy Base =
25+
static_cast<FloatTy>(std::numeric_limits<IntegerTy>::max()) + 1.0;
26+
FloatTy Conv = std::clamp(floor(Val * Base), static_cast<FloatTy>(0.0),
27+
static_cast<FloatTy>(255.0));
28+
return static_cast<IntegerTy>(Conv);
29+
}
30+
31+
TEST(ColorTests, RoundTrip) {
32+
{
33+
Color RGB = Color(1.0, 0.5, 0.0);
34+
Color XYZ = RGB.translateSpace(Color::XYZ);
35+
Color LAB = RGB.translateSpace(Color::LAB);
36+
37+
Color XYZPrime = LAB.translateSpace(Color::XYZ);
38+
Color RGBPrime = LAB.translateSpace(Color::RGB);
39+
40+
// The floating point rounding errors across these transformations are
41+
// significant, so rather than comparing the float values we normalize them
42+
// back into integer color values to compare.
43+
EXPECT_EQ(NormalizeToInteger<uint16_t>(XYZ.R),
44+
NormalizeToInteger<uint16_t>(XYZPrime.R));
45+
EXPECT_EQ(NormalizeToInteger<uint16_t>(XYZ.G),
46+
NormalizeToInteger<uint16_t>(XYZPrime.G));
47+
EXPECT_EQ(NormalizeToInteger<uint16_t>(XYZ.B),
48+
NormalizeToInteger<uint16_t>(XYZPrime.B));
49+
50+
EXPECT_EQ(NormalizeToInteger<uint16_t>(RGB.R),
51+
NormalizeToInteger<uint16_t>(RGBPrime.R));
52+
EXPECT_EQ(NormalizeToInteger<uint16_t>(RGB.G),
53+
NormalizeToInteger<uint16_t>(RGBPrime.G));
54+
EXPECT_EQ(NormalizeToInteger<uint16_t>(RGB.B),
55+
NormalizeToInteger<uint16_t>(RGBPrime.B));
56+
}
57+
58+
{
59+
Color RGB = Color(0.125, 0.896, 0.652);
60+
Color XYZ = RGB.translateSpace(Color::XYZ);
61+
Color LAB = RGB.translateSpace(Color::LAB);
62+
63+
Color XYZPrime = LAB.translateSpace(Color::XYZ);
64+
Color RGBPrime = LAB.translateSpace(Color::RGB);
65+
66+
// The floating point rounding errors across these transformations are
67+
// significant, so rather than comparing the float values we normalize them
68+
// back into integer color values to compare.
69+
EXPECT_EQ(NormalizeToInteger<uint16_t>(XYZ.R),
70+
NormalizeToInteger<uint16_t>(XYZPrime.R));
71+
EXPECT_EQ(NormalizeToInteger<uint16_t>(XYZ.G),
72+
NormalizeToInteger<uint16_t>(XYZPrime.G));
73+
EXPECT_EQ(NormalizeToInteger<uint16_t>(XYZ.B),
74+
NormalizeToInteger<uint16_t>(XYZPrime.B));
75+
76+
EXPECT_EQ(NormalizeToInteger<uint16_t>(RGB.R),
77+
NormalizeToInteger<uint16_t>(RGBPrime.R));
78+
EXPECT_EQ(NormalizeToInteger<uint16_t>(RGB.G),
79+
NormalizeToInteger<uint16_t>(RGBPrime.G));
80+
EXPECT_EQ(NormalizeToInteger<uint16_t>(RGB.B),
81+
NormalizeToInteger<uint16_t>(RGBPrime.B));
82+
}
83+
}

0 commit comments

Comments
 (0)