summaryrefslogtreecommitdiff
path: root/lib/jxl/test_utils.h
diff options
context:
space:
mode:
Diffstat (limited to 'lib/jxl/test_utils.h')
-rw-r--r--lib/jxl/test_utils.h442
1 files changed, 331 insertions, 111 deletions
diff --git a/lib/jxl/test_utils.h b/lib/jxl/test_utils.h
index e7e8f67..b55cc3d 100644
--- a/lib/jxl/test_utils.h
+++ b/lib/jxl/test_utils.h
@@ -8,22 +8,30 @@
// Macros and functions useful for tests.
-#include <random>
-
+// gmock unconditionally redefines those macros (to wrong values).
+// Lets include it only here and mitigate the problem.
+#pragma push_macro("PRIdS")
+#pragma push_macro("PRIuS")
#include "gmock/gmock.h"
+#pragma pop_macro("PRIuS")
+#pragma pop_macro("PRIdS")
+
#include "gtest/gtest.h"
#include "jxl/codestream_header.h"
#include "jxl/encode.h"
+#include "lib/extras/dec/jxl.h"
+#include "lib/extras/packed_image_convert.h"
#include "lib/jxl/aux_out_fwd.h"
#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/random.h"
#include "lib/jxl/codec_in_out.h"
#include "lib/jxl/color_encoding_internal.h"
#include "lib/jxl/common.h" // JPEGXL_ENABLE_TRANSCODE_JPEG
-#include "lib/jxl/dec_file.h"
-#include "lib/jxl/dec_params.h"
+#include "lib/jxl/enc_color_management.h"
#include "lib/jxl/enc_external_image.h"
#include "lib/jxl/enc_file.h"
#include "lib/jxl/enc_params.h"
+#include "lib/jxl/test_image.h"
#ifdef JXL_DISABLE_SLOW_TESTS
#define JXL_SLOW_TEST(X) DISABLED_##X
@@ -51,6 +59,10 @@
#define JXL_GTEST_INSTANTIATE_TEST_SUITE_P INSTANTIATE_TEST_CASE_P
#endif
+// Ensures that we don't make our test bounds too lax, effectively disabling the
+// tests.
+MATCHER_P(IsSlightlyBelow, max, "") { return max * 0.75 <= arg && arg <= max; }
+
namespace jxl {
namespace test {
@@ -74,22 +86,18 @@ void JxlBasicInfoSetFromPixelFormat(JxlBasicInfo* basic_info,
basic_info->bits_per_sample = 16;
basic_info->exponent_bits_per_sample = 0;
break;
- case JXL_TYPE_UINT32:
- basic_info->bits_per_sample = 32;
- basic_info->exponent_bits_per_sample = 0;
- break;
- case JXL_TYPE_BOOLEAN:
- basic_info->bits_per_sample = 1;
- basic_info->exponent_bits_per_sample = 0;
- break;
+ default:
+ JXL_ABORT("Unhandled JxlDataType");
+ }
+ if (pixel_format->num_channels < 3) {
+ basic_info->num_color_channels = 1;
+ } else {
+ basic_info->num_color_channels = 3;
}
if (pixel_format->num_channels == 2 || pixel_format->num_channels == 4) {
- basic_info->alpha_exponent_bits = 0;
- if (basic_info->bits_per_sample == 32) {
- basic_info->alpha_bits = 16;
- } else {
- basic_info->alpha_bits = basic_info->bits_per_sample;
- }
+ basic_info->alpha_exponent_bits = basic_info->exponent_bits_per_sample;
+ basic_info->alpha_bits = basic_info->bits_per_sample;
+ basic_info->num_extra_channels = 1;
} else {
basic_info->alpha_exponent_bits = 0;
basic_info->alpha_bits = 0;
@@ -97,8 +105,9 @@ void JxlBasicInfoSetFromPixelFormat(JxlBasicInfo* basic_info,
}
MATCHER_P(MatchesPrimariesAndTransferFunction, color_encoding, "") {
- return arg.primaries == color_encoding.primaries &&
- arg.tf.IsSame(color_encoding.tf);
+ return (arg.ICC() == color_encoding.ICC() ||
+ (arg.primaries == color_encoding.primaries &&
+ arg.tf.IsSame(color_encoding.tf)));
}
MATCHER(MatchesPrimariesAndTransferFunction, "") {
@@ -107,9 +116,23 @@ MATCHER(MatchesPrimariesAndTransferFunction, "") {
result_listener);
}
+template <typename Source>
+Status DecodeFile(extras::JXLDecompressParams dparams, const Source& file,
+ CodecInOut* JXL_RESTRICT io, ThreadPool* pool) {
+ if (pool && !dparams.runner_opaque) {
+ dparams.runner = pool->runner();
+ dparams.runner_opaque = pool->runner_opaque();
+ }
+ extras::PackedPixelFile ppf;
+ JXL_RETURN_IF_ERROR(DecodeImageJXL(file.data(), file.size(), dparams,
+ /*decoded_bytes=*/nullptr, &ppf));
+ JXL_RETURN_IF_ERROR(ConvertPackedPixelFileToCodecInOut(ppf, pool, io));
+ return true;
+}
+
// Returns compressed size [bytes].
size_t Roundtrip(const CodecInOut* io, const CompressParams& cparams,
- const DecompressParams& dparams, ThreadPool* pool,
+ extras::JXLDecompressParams dparams, ThreadPool* pool,
CodecInOut* JXL_RESTRICT io2, AuxOut* aux_out = nullptr) {
PaddedBytes compressed;
@@ -124,8 +147,8 @@ size_t Roundtrip(const CodecInOut* io, const CompressParams& cparams,
std::unique_ptr<PassesEncoderState> enc_state =
jxl::make_unique<PassesEncoderState>();
- EXPECT_TRUE(
- EncodeFile(cparams, io, enc_state.get(), &compressed, aux_out, pool));
+ EXPECT_TRUE(EncodeFile(cparams, io, enc_state.get(), &compressed, GetJxlCms(),
+ aux_out, pool));
std::vector<ColorEncoding> metadata_encodings_1;
for (const ImageBundle& ib1 : io->frames) {
@@ -213,6 +236,7 @@ static inline ColorEncoding ColorEncodingFromDescriptor(
c.primaries = desc.primaries;
c.tf.SetTransferFunction(desc.tf);
c.rendering_intent = desc.rendering_intent;
+ JXL_CHECK(c.CreateICC());
return c;
}
@@ -269,89 +293,6 @@ std::vector<ColorEncodingDescriptor> AllEncodings() {
return all_encodings;
}
-// Returns a test image with some autogenerated pixel content, using 16 bits per
-// channel, big endian order, 1 to 4 channels
-// The seed parameter allows to create images with different pixel content.
-std::vector<uint8_t> GetSomeTestImage(size_t xsize, size_t ysize,
- size_t num_channels, uint16_t seed) {
- // Cause more significant image difference for successive seeds.
- std::mt19937 std_rng(seed);
- std::uniform_int_distribution<uint16_t> std_distr(0, 65535);
-
- // Returns random integer in interval (0, max_value - 1)
- auto rng = [&std_rng, &std_distr](size_t max_value) -> size_t {
- return static_cast<size_t>(std_distr(std_rng) / 65536.0f * max_value);
- };
-
- // Dark background gradient color
- uint16_t r0 = rng(32768);
- uint16_t g0 = rng(32768);
- uint16_t b0 = rng(32768);
- uint16_t a0 = rng(32768);
- uint16_t r1 = rng(32768);
- uint16_t g1 = rng(32768);
- uint16_t b1 = rng(32768);
- uint16_t a1 = rng(32768);
-
- // Circle with different color
- size_t circle_x = rng(xsize);
- size_t circle_y = rng(ysize);
- size_t circle_r = rng(std::min(xsize, ysize));
-
- // Rectangle with random noise
- size_t rect_x0 = rng(xsize);
- size_t rect_y0 = rng(ysize);
- size_t rect_x1 = rng(xsize);
- size_t rect_y1 = rng(ysize);
- if (rect_x1 < rect_x0) std::swap(rect_x0, rect_y1);
- if (rect_y1 < rect_y0) std::swap(rect_y0, rect_y1);
-
- size_t num_pixels = xsize * ysize;
- // 16 bits per channel, big endian, 4 channels
- std::vector<uint8_t> pixels(num_pixels * num_channels * 2);
- // Create pixel content to test, actual content does not matter as long as it
- // can be compared after roundtrip.
- for (size_t y = 0; y < ysize; y++) {
- for (size_t x = 0; x < xsize; x++) {
- uint16_t r = r0 * (ysize - y - 1) / ysize + r1 * y / ysize;
- uint16_t g = g0 * (ysize - y - 1) / ysize + g1 * y / ysize;
- uint16_t b = b0 * (ysize - y - 1) / ysize + b1 * y / ysize;
- uint16_t a = a0 * (ysize - y - 1) / ysize + a1 * y / ysize;
- // put some shape in there for visual debugging
- if ((x - circle_x) * (x - circle_x) + (y - circle_y) * (y - circle_y) <
- circle_r * circle_r) {
- r = (65535 - x * y) ^ seed;
- g = (x << 8) + y + seed;
- b = (y << 8) + x * seed;
- a = 32768 + x * 256 - y;
- } else if (x > rect_x0 && x < rect_x1 && y > rect_y0 && y < rect_y1) {
- r = rng(65536);
- g = rng(65536);
- b = rng(65536);
- a = rng(65536);
- }
- size_t i = (y * xsize + x) * 2 * num_channels;
- pixels[i + 0] = (r >> 8);
- pixels[i + 1] = (r & 255);
- if (num_channels >= 2) {
- // This may store what is called 'g' in the alpha channel of a 2-channel
- // image, but that's ok since the content is arbitrary
- pixels[i + 2] = (g >> 8);
- pixels[i + 3] = (g & 255);
- }
- if (num_channels >= 3) {
- pixels[i + 4] = (b >> 8);
- pixels[i + 5] = (b & 255);
- }
- if (num_channels >= 4) {
- pixels[i + 6] = (a >> 8);
- pixels[i + 7] = (a & 255);
- }
- }
- }
- return pixels;
-}
-
// Returns a CodecInOut based on the buf, xsize, ysize, and the assumption
// that the buffer was created using `GetSomeTestImage`.
jxl::CodecInOut SomeTestImageToCodecInOut(const std::vector<uint8_t>& buf,
@@ -364,15 +305,294 @@ jxl::CodecInOut SomeTestImageToCodecInOut(const std::vector<uint8_t>& buf,
/*is_gray=*/num_channels == 1 || num_channels == 2);
EXPECT_TRUE(ConvertFromExternal(
jxl::Span<const uint8_t>(buf.data(), buf.size()), xsize, ysize,
- jxl::ColorEncoding::SRGB(/*is_gray=*/num_channels == 1 ||
- num_channels == 2),
- /*has_alpha=*/num_channels == 2 || num_channels == 4,
+ jxl::ColorEncoding::SRGB(/*is_gray=*/num_channels < 3), num_channels,
/*alpha_is_premultiplied=*/false, /*bits_per_sample=*/16, JXL_BIG_ENDIAN,
- /*flipped_y=*/false, /*pool=*/nullptr,
- /*ib=*/&io.Main(), /*float_in=*/false));
+ /*pool=*/nullptr,
+ /*ib=*/&io.Main(), /*float_in=*/false, 0));
return io;
}
+bool Near(double expected, double value, double max_dist) {
+ double dist = expected > value ? expected - value : value - expected;
+ return dist <= max_dist;
+}
+
+// Loads a Big-Endian float
+float LoadBEFloat(const uint8_t* p) {
+ uint32_t u = LoadBE32(p);
+ float result;
+ memcpy(&result, &u, 4);
+ return result;
+}
+
+// Loads a Little-Endian float
+float LoadLEFloat(const uint8_t* p) {
+ uint32_t u = LoadLE32(p);
+ float result;
+ memcpy(&result, &u, 4);
+ return result;
+}
+
+// Based on highway scalar implementation, for testing
+float LoadFloat16(uint16_t bits16) {
+ const uint32_t sign = bits16 >> 15;
+ const uint32_t biased_exp = (bits16 >> 10) & 0x1F;
+ const uint32_t mantissa = bits16 & 0x3FF;
+
+ // Subnormal or zero
+ if (biased_exp == 0) {
+ const float subnormal = (1.0f / 16384) * (mantissa * (1.0f / 1024));
+ return sign ? -subnormal : subnormal;
+ }
+
+ // Normalized: convert the representation directly (faster than ldexp/tables).
+ const uint32_t biased_exp32 = biased_exp + (127 - 15);
+ const uint32_t mantissa32 = mantissa << (23 - 10);
+ const uint32_t bits32 = (sign << 31) | (biased_exp32 << 23) | mantissa32;
+
+ float result;
+ memcpy(&result, &bits32, 4);
+ return result;
+}
+
+float LoadLEFloat16(const uint8_t* p) {
+ uint16_t bits16 = LoadLE16(p);
+ return LoadFloat16(bits16);
+}
+
+float LoadBEFloat16(const uint8_t* p) {
+ uint16_t bits16 = LoadBE16(p);
+ return LoadFloat16(bits16);
+}
+
+size_t GetPrecision(JxlDataType data_type) {
+ switch (data_type) {
+ case JXL_TYPE_UINT8:
+ return 8;
+ case JXL_TYPE_UINT16:
+ return 16;
+ case JXL_TYPE_FLOAT:
+ // Floating point mantissa precision
+ return 24;
+ case JXL_TYPE_FLOAT16:
+ return 11;
+ default:
+ JXL_ABORT("Unhandled JxlDataType");
+ }
+}
+
+size_t GetDataBits(JxlDataType data_type) {
+ switch (data_type) {
+ case JXL_TYPE_UINT8:
+ return 8;
+ case JXL_TYPE_UINT16:
+ return 16;
+ case JXL_TYPE_FLOAT:
+ return 32;
+ case JXL_TYPE_FLOAT16:
+ return 16;
+ default:
+ JXL_ABORT("Unhandled JxlDataType");
+ }
+}
+
+// Procedure to convert pixels to double precision, not efficient, but
+// well-controlled for testing. It uses double, to be able to represent all
+// precisions needed for the maximum data types the API supports: uint32_t
+// integers, and, single precision float. The values are in range 0-1 for SDR.
+std::vector<double> ConvertToRGBA32(const uint8_t* pixels, size_t xsize,
+ size_t ysize, const JxlPixelFormat& format,
+ double factor = 0.0) {
+ std::vector<double> result(xsize * ysize * 4);
+ size_t num_channels = format.num_channels;
+ bool gray = num_channels == 1 || num_channels == 2;
+ bool alpha = num_channels == 2 || num_channels == 4;
+
+ size_t stride =
+ xsize * jxl::DivCeil(GetDataBits(format.data_type) * num_channels,
+ jxl::kBitsPerByte);
+ if (format.align > 1) stride = jxl::RoundUpTo(stride, format.align);
+
+ if (format.data_type == JXL_TYPE_UINT8) {
+ // Multiplier to bring to 0-1.0 range
+ double mul = factor > 0.0 ? factor : 1.0 / 255.0;
+ for (size_t y = 0; y < ysize; ++y) {
+ for (size_t x = 0; x < xsize; ++x) {
+ size_t j = (y * xsize + x) * 4;
+ size_t i = y * stride + x * num_channels;
+ double r = pixels[i];
+ double g = gray ? r : pixels[i + 1];
+ double b = gray ? r : pixels[i + 2];
+ double a = alpha ? pixels[i + num_channels - 1] : 255;
+ result[j + 0] = r * mul;
+ result[j + 1] = g * mul;
+ result[j + 2] = b * mul;
+ result[j + 3] = a * mul;
+ }
+ }
+ } else if (format.data_type == JXL_TYPE_UINT16) {
+ // Multiplier to bring to 0-1.0 range
+ double mul = factor > 0.0 ? factor : 1.0 / 65535.0;
+ for (size_t y = 0; y < ysize; ++y) {
+ for (size_t x = 0; x < xsize; ++x) {
+ size_t j = (y * xsize + x) * 4;
+ size_t i = y * stride + x * num_channels * 2;
+ double r, g, b, a;
+ if (format.endianness == JXL_BIG_ENDIAN) {
+ r = (pixels[i + 0] << 8) + pixels[i + 1];
+ g = gray ? r : (pixels[i + 2] << 8) + pixels[i + 3];
+ b = gray ? r : (pixels[i + 4] << 8) + pixels[i + 5];
+ a = alpha ? (pixels[i + num_channels * 2 - 2] << 8) +
+ pixels[i + num_channels * 2 - 1]
+ : 65535;
+ } else {
+ r = (pixels[i + 1] << 8) + pixels[i + 0];
+ g = gray ? r : (pixels[i + 3] << 8) + pixels[i + 2];
+ b = gray ? r : (pixels[i + 5] << 8) + pixels[i + 4];
+ a = alpha ? (pixels[i + num_channels * 2 - 1] << 8) +
+ pixels[i + num_channels * 2 - 2]
+ : 65535;
+ }
+ result[j + 0] = r * mul;
+ result[j + 1] = g * mul;
+ result[j + 2] = b * mul;
+ result[j + 3] = a * mul;
+ }
+ }
+ } else if (format.data_type == JXL_TYPE_FLOAT) {
+ for (size_t y = 0; y < ysize; ++y) {
+ for (size_t x = 0; x < xsize; ++x) {
+ size_t j = (y * xsize + x) * 4;
+ size_t i = y * stride + x * num_channels * 4;
+ double r, g, b, a;
+ if (format.endianness == JXL_BIG_ENDIAN) {
+ r = LoadBEFloat(pixels + i);
+ g = gray ? r : LoadBEFloat(pixels + i + 4);
+ b = gray ? r : LoadBEFloat(pixels + i + 8);
+ a = alpha ? LoadBEFloat(pixels + i + num_channels * 4 - 4) : 1.0;
+ } else {
+ r = LoadLEFloat(pixels + i);
+ g = gray ? r : LoadLEFloat(pixels + i + 4);
+ b = gray ? r : LoadLEFloat(pixels + i + 8);
+ a = alpha ? LoadLEFloat(pixels + i + num_channels * 4 - 4) : 1.0;
+ }
+ result[j + 0] = r;
+ result[j + 1] = g;
+ result[j + 2] = b;
+ result[j + 3] = a;
+ }
+ }
+ } else if (format.data_type == JXL_TYPE_FLOAT16) {
+ for (size_t y = 0; y < ysize; ++y) {
+ for (size_t x = 0; x < xsize; ++x) {
+ size_t j = (y * xsize + x) * 4;
+ size_t i = y * stride + x * num_channels * 2;
+ double r, g, b, a;
+ if (format.endianness == JXL_BIG_ENDIAN) {
+ r = LoadBEFloat16(pixels + i);
+ g = gray ? r : LoadBEFloat16(pixels + i + 2);
+ b = gray ? r : LoadBEFloat16(pixels + i + 4);
+ a = alpha ? LoadBEFloat16(pixels + i + num_channels * 2 - 2) : 1.0;
+ } else {
+ r = LoadLEFloat16(pixels + i);
+ g = gray ? r : LoadLEFloat16(pixels + i + 2);
+ b = gray ? r : LoadLEFloat16(pixels + i + 4);
+ a = alpha ? LoadLEFloat16(pixels + i + num_channels * 2 - 2) : 1.0;
+ }
+ result[j + 0] = r;
+ result[j + 1] = g;
+ result[j + 2] = b;
+ result[j + 3] = a;
+ }
+ }
+ } else {
+ JXL_ASSERT(false); // Unsupported type
+ }
+ return result;
+}
+// Returns amount of pixels which differ between the two pictures. Image b is
+// the image after roundtrip after roundtrip, image a before roundtrip. There
+// are more strict requirements for the alpha channel and grayscale values of
+// the output image.
+size_t ComparePixels(const uint8_t* a, const uint8_t* b, size_t xsize,
+ size_t ysize, const JxlPixelFormat& format_a,
+ const JxlPixelFormat& format_b,
+ double threshold_multiplier = 1.0) {
+ // Convert both images to equal full precision for comparison.
+ std::vector<double> a_full = ConvertToRGBA32(a, xsize, ysize, format_a);
+ std::vector<double> b_full = ConvertToRGBA32(b, xsize, ysize, format_b);
+ bool gray_a = format_a.num_channels < 3;
+ bool gray_b = format_b.num_channels < 3;
+ bool alpha_a = !(format_a.num_channels & 1);
+ bool alpha_b = !(format_b.num_channels & 1);
+ size_t bits_a = GetPrecision(format_a.data_type);
+ size_t bits_b = GetPrecision(format_b.data_type);
+ size_t bits = std::min(bits_a, bits_b);
+ // How much distance is allowed in case of pixels with lower bit depths, given
+ // that the double precision float images use range 0-1.0.
+ // E.g. in case of 1-bit this is 0.5 since 0.499 must map to 0 and 0.501 must
+ // map to 1.
+ double precision = 0.5 * threshold_multiplier / ((1ull << bits) - 1ull);
+ if (format_a.data_type == JXL_TYPE_FLOAT16 ||
+ format_b.data_type == JXL_TYPE_FLOAT16) {
+ // Lower the precision for float16, because it currently looks like the
+ // scalar and wasm implementations of hwy have 1 less bit of precision
+ // than the x86 implementations.
+ // TODO(lode): Set the required precision back to 11 bits when possible.
+ precision = 0.5 * threshold_multiplier / ((1ull << (bits - 1)) - 1ull);
+ }
+ size_t numdiff = 0;
+ for (size_t y = 0; y < ysize; y++) {
+ for (size_t x = 0; x < xsize; x++) {
+ size_t i = (y * xsize + x) * 4;
+ bool ok = true;
+ if (gray_a || gray_b) {
+ if (!Near(a_full[i + 0], b_full[i + 0], precision)) ok = false;
+ // If the input was grayscale and the output not, then the output must
+ // have all channels equal.
+ if (gray_a && b_full[i + 0] != b_full[i + 1] &&
+ b_full[i + 2] != b_full[i + 2]) {
+ ok = false;
+ }
+ } else {
+ if (!Near(a_full[i + 0], b_full[i + 0], precision) ||
+ !Near(a_full[i + 1], b_full[i + 1], precision) ||
+ !Near(a_full[i + 2], b_full[i + 2], precision)) {
+ ok = false;
+ }
+ }
+ if (alpha_a && alpha_b) {
+ if (!Near(a_full[i + 3], b_full[i + 3], precision)) ok = false;
+ } else {
+ // If the input had no alpha channel, the output should be opaque
+ // after roundtrip.
+ if (alpha_b && !Near(1.0, b_full[i + 3], precision)) ok = false;
+ }
+ if (!ok) numdiff++;
+ }
+ }
+ return numdiff;
+}
+double DistanceRMS(const uint8_t* a, const uint8_t* b, size_t xsize,
+ size_t ysize, const JxlPixelFormat& format) {
+ // Convert both images to equal full precision for comparison.
+ std::vector<double> a_full = ConvertToRGBA32(a, xsize, ysize, format);
+ std::vector<double> b_full = ConvertToRGBA32(b, xsize, ysize, format);
+ double sum = 0.0;
+ for (size_t y = 0; y < ysize; y++) {
+ double row_sum = 0.0;
+ for (size_t x = 0; x < xsize; x++) {
+ size_t i = (y * xsize + x) * 4;
+ for (size_t c = 0; c < format.num_channels; ++c) {
+ double diff = a_full[i + c] - b_full[i + c];
+ row_sum += diff * diff;
+ }
+ }
+ sum += row_sum;
+ }
+ sum /= (xsize * ysize);
+ return sqrt(sum);
+}
} // namespace test
bool operator==(const jxl::PaddedBytes& a, const jxl::PaddedBytes& b) {