Skip to content

Commit c08696f

Browse files
committed
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.
1 parent 64f08ba commit c08696f

File tree

2 files changed

+50
-1
lines changed

2 files changed

+50
-1
lines changed

Tests/LibGfx/TestBilevelImage.cpp

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
* SPDX-License-Identifier: BSD-2-Clause
55
*/
66

7+
#include <LibGfx/Bitmap.h>
8+
#include <LibGfx/ICC/TagTypes.h>
9+
#include <LibGfx/ICC/WellKnownProfiles.h>
710
#include <LibGfx/ImageFormats/BilevelImage.h>
811
#include <LibTest/TestCase.h>
912

@@ -60,3 +63,45 @@ TEST_CASE(get_bits_over_8bits)
6063
EXPECT_EQ(bilevel->get_bits(8, 0, 8), 0xFE);
6164
EXPECT_EQ(bilevel->get_bits(12, 0, 4), 0xE);
6265
}
66+
67+
static ErrorOr<void> test_bayer_dither(Gfx::DitheringAlgorithm algorithm, u32 size)
68+
{
69+
auto srgb_curve = TRY(Gfx::ICC::sRGB_curve());
70+
auto bitmap = TRY(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, { size, size }));
71+
auto number_of_states = size * size + 1;
72+
73+
auto test_luminosity = [&](f32 input_luminosity, f32 output_luminosity) -> ErrorOr<void> {
74+
auto uncompressed = round_to<u8>(srgb_curve->evaluate_inverse(input_luminosity) * 255);
75+
bitmap->fill(Gfx::Color(uncompressed, uncompressed, uncompressed));
76+
auto bilevel = TRY(Gfx::BilevelImage::create_from_bitmap(bitmap, algorithm));
77+
double average = 0;
78+
for (u32 y = 0; y < bilevel->height(); ++y) {
79+
for (u32 x = 0; x < bilevel->width(); ++x)
80+
average += bilevel->get_bit(x, y) ? 0 : 1;
81+
}
82+
83+
EXPECT_APPROXIMATE(average / (size * size), output_luminosity);
84+
return {};
85+
};
86+
87+
// Test full black and full white.
88+
TRY(test_luminosity(0, 0));
89+
TRY(test_luminosity(1, 1));
90+
91+
// Test all states at half the range.
92+
for (u32 s = 0; s < number_of_states; ++s) {
93+
// We test all states in the middle of there range.
94+
auto value = (static_cast<f32>(s) + 0.5f) / number_of_states;
95+
// There are only (number_of_states - 1) states of luminosity.
96+
TRY(test_luminosity(value, static_cast<f32>(s) / (number_of_states - 1)));
97+
}
98+
return {};
99+
}
100+
101+
TEST_CASE(bayer_dither)
102+
{
103+
104+
TRY_OR_FAIL(test_bayer_dither(Gfx::DitheringAlgorithm::Bayer2x2, 2));
105+
TRY_OR_FAIL(test_bayer_dither(Gfx::DitheringAlgorithm::Bayer4x4, 4));
106+
TRY_OR_FAIL(test_bayer_dither(Gfx::DitheringAlgorithm::Bayer8x8, 8));
107+
}

Userland/Libraries/LibGfx/ImageFormats/BilevelImage.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,9 +312,13 @@ ErrorOr<NonnullRefPtr<BilevelImage>> BilevelImage::create_from_bitmap(Gfx::Bitma
312312
VERIFY(is_power_of_two(n));
313313
auto mask = n - 1;
314314

315+
// A bayer matrix of dimension N has N x N +1 different states. First one
316+
// is an all black matrix, and then one more for each element turning white.
317+
u32 number_of_states = n * n + 1;
318+
315319
for (int y = 0, i = 0; y < bitmap.height(); ++y) {
316320
for (int x = 0; x < bitmap.width(); ++x, ++i) {
317-
u8 threshold = (bayer_matrix[(y & mask) * n + (x & mask)] * 255) / ((n * n) - 1);
321+
u8 threshold = round_to<u8>((1 + bayer_matrix[(y & mask) * n + (x & mask)]) * 255.f / number_of_states);
318322
bilevel_image->set_bit(x, y, gray_bitmap[i] > threshold ? 0 : 1);
319323
}
320324
}

0 commit comments

Comments
 (0)