From c08696f60f7c1b17dffa697497c17f57f34a491c Mon Sep 17 00:00:00 2001 From: Lucas CHOLLET Date: Thu, 27 Nov 2025 11:37:01 +0100 Subject: [PATCH] LibGfx: Make Bayer dithering more luminosity-conservative On average, this patch reduces the error in luminosity between an input image and its dithered equivalent. This is done by correcting an off-by- one error in the threshold computation and also by making sure the range of mapped gray values to matrix pattern is of equal size for all ranges. This means that all values between 0 and 255 / (N * N + 1) (51 for Bayer2x2) will result in a black pixel while only a value of 0 would previously do it. The test works by first generating gray bitmap, then applying Bayer dithering on them and finally compare the luminosity of the two. --- Tests/LibGfx/TestBilevelImage.cpp | 45 +++++++++++++++++++ .../LibGfx/ImageFormats/BilevelImage.cpp | 6 ++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/Tests/LibGfx/TestBilevelImage.cpp b/Tests/LibGfx/TestBilevelImage.cpp index f7dce5fa0f5f23..69c82b90b1a744 100644 --- a/Tests/LibGfx/TestBilevelImage.cpp +++ b/Tests/LibGfx/TestBilevelImage.cpp @@ -4,6 +4,9 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include +#include +#include #include #include @@ -60,3 +63,45 @@ TEST_CASE(get_bits_over_8bits) EXPECT_EQ(bilevel->get_bits(8, 0, 8), 0xFE); EXPECT_EQ(bilevel->get_bits(12, 0, 4), 0xE); } + +static ErrorOr test_bayer_dither(Gfx::DitheringAlgorithm algorithm, u32 size) +{ + auto srgb_curve = TRY(Gfx::ICC::sRGB_curve()); + auto bitmap = TRY(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, { size, size })); + auto number_of_states = size * size + 1; + + auto test_luminosity = [&](f32 input_luminosity, f32 output_luminosity) -> ErrorOr { + auto uncompressed = round_to(srgb_curve->evaluate_inverse(input_luminosity) * 255); + bitmap->fill(Gfx::Color(uncompressed, uncompressed, uncompressed)); + auto bilevel = TRY(Gfx::BilevelImage::create_from_bitmap(bitmap, algorithm)); + double average = 0; + for (u32 y = 0; y < bilevel->height(); ++y) { + for (u32 x = 0; x < bilevel->width(); ++x) + average += bilevel->get_bit(x, y) ? 0 : 1; + } + + EXPECT_APPROXIMATE(average / (size * size), output_luminosity); + return {}; + }; + + // Test full black and full white. + TRY(test_luminosity(0, 0)); + TRY(test_luminosity(1, 1)); + + // Test all states at half the range. + for (u32 s = 0; s < number_of_states; ++s) { + // We test all states in the middle of there range. + auto value = (static_cast(s) + 0.5f) / number_of_states; + // There are only (number_of_states - 1) states of luminosity. + TRY(test_luminosity(value, static_cast(s) / (number_of_states - 1))); + } + return {}; +} + +TEST_CASE(bayer_dither) +{ + + TRY_OR_FAIL(test_bayer_dither(Gfx::DitheringAlgorithm::Bayer2x2, 2)); + TRY_OR_FAIL(test_bayer_dither(Gfx::DitheringAlgorithm::Bayer4x4, 4)); + TRY_OR_FAIL(test_bayer_dither(Gfx::DitheringAlgorithm::Bayer8x8, 8)); +} diff --git a/Userland/Libraries/LibGfx/ImageFormats/BilevelImage.cpp b/Userland/Libraries/LibGfx/ImageFormats/BilevelImage.cpp index 4884c34c1fe594..b23c605f920eff 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/BilevelImage.cpp +++ b/Userland/Libraries/LibGfx/ImageFormats/BilevelImage.cpp @@ -312,9 +312,13 @@ ErrorOr> BilevelImage::create_from_bitmap(Gfx::Bitma VERIFY(is_power_of_two(n)); auto mask = n - 1; + // A bayer matrix of dimension N has N x N +1 different states. First one + // is an all black matrix, and then one more for each element turning white. + u32 number_of_states = n * n + 1; + for (int y = 0, i = 0; y < bitmap.height(); ++y) { for (int x = 0; x < bitmap.width(); ++x, ++i) { - u8 threshold = (bayer_matrix[(y & mask) * n + (x & mask)] * 255) / ((n * n) - 1); + u8 threshold = round_to((1 + bayer_matrix[(y & mask) * n + (x & mask)]) * 255.f / number_of_states); bilevel_image->set_bit(x, y, gray_bitmap[i] > threshold ? 0 : 1); } }