Skip to content

Commit f1aba02

Browse files
committed
LibGfx/JPEG: Add a deringing pass to the encoder
The idea is described here https://kornel.ski/deringing/. And allows reducing the noise around sharp white edges. This is visible when encoding an image like `buggie.png`. One nice aspect of the optimization is that it only affects macroblocks where it can help.
1 parent 997cc8f commit f1aba02

File tree

2 files changed

+74
-3
lines changed

2 files changed

+74
-3
lines changed

Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.cpp

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,40 @@ class JPEGBigEndianOutputBitStream : public Stream {
9898
size_t m_bit_offset { 0 };
9999
};
100100

101+
void interpolate(f32* component, f32 max_value, i8 start, i8 stop)
102+
{
103+
// We're creating a uniform (ɑ = 1) Catmull–Rom curve for the missing points.
104+
// That means that tᵢ₊₁ = tᵢ + 1.
105+
// Note that component[start] should be interpolated but component[stop] should not.
106+
107+
// p1 and p2 are set to the ceil value.
108+
// p0 is set to the last non-max value if possible, otherwise the value of p3 for symmetry.
109+
// The same logic is applied to p3.
110+
f32 const p0 = start == 0 ? component[zigzag_map[stop]] : component[zigzag_map[start - 1]];
111+
f32 const p1 = max_value;
112+
f32 const p2 = max_value;
113+
f32 const p3 = stop > 63 ? p0 : component[zigzag_map[stop]];
114+
115+
f32 const t0 = 0.0f;
116+
f32 const t1 = 1;
117+
f32 const t2 = 2;
118+
f32 const t3 = 3;
119+
120+
f32 const step = 1. / (stop - start + 1);
121+
f32 t = t1;
122+
for (i8 i = start; i < stop; ++i) {
123+
t += step;
124+
f32 const A1 = p0 * (t1 - t) / (t1 - t0) + p1 * (t - t0) / (t1 - t0);
125+
f32 const A2 = p1 * (t2 - t) / (t2 - t1) + p2 * (t - t1) / (t2 - t1);
126+
f32 const A3 = p2 * (t3 - t) / (t3 - t2) + p3 * (t - t2) / (t3 - t2);
127+
f32 const B1 = A1 * (t2 - t) / (t2 - t0) + A2 * (t - t0) / (t2 - t0);
128+
f32 const B2 = A2 * (t3 - t) / (t3 - t1) + A3 * (t - t1) / (t3 - t1);
129+
f32 const C = B1 * (t2 - t) / (t2 - t1) + B2 * (t - t1) / (t2 - t1);
130+
131+
component[zigzag_map[i]] = C;
132+
}
133+
}
134+
101135
class JPEGEncodingContext {
102136
public:
103137
JPEGEncodingContext(JPEGBigEndianOutputBitStream output_stream)
@@ -256,6 +290,35 @@ class JPEGEncodingContext {
256290
}
257291
}
258292

293+
void apply_deringing()
294+
{
295+
// The method used here is described at: https://kornel.ski/deringing/.
296+
297+
for (auto& macroblock : m_macroblocks) {
298+
for (auto component : { macroblock.r, macroblock.g, macroblock.b }) {
299+
static constexpr auto maximum_value = NumericLimits<u8>::max();
300+
Optional<u8> start;
301+
u8 i = 0;
302+
for (; i < 64; ++i) {
303+
if (component[zigzag_map[i]] == maximum_value) {
304+
if (!start.has_value())
305+
start = i;
306+
else
307+
continue;
308+
} else {
309+
if (start.has_value() && i - *start > 2) {
310+
interpolate(component, maximum_value, *start, i);
311+
}
312+
start.clear();
313+
}
314+
}
315+
316+
if (start != 0 && component[zigzag_map[63]] == maximum_value)
317+
interpolate(component, maximum_value, *start, 64);
318+
}
319+
}
320+
}
321+
259322
ErrorOr<void> write_huffman_stream(Mode mode)
260323
{
261324
for (auto& float_macroblock : m_macroblocks) {
@@ -633,8 +696,10 @@ ErrorOr<void> add_headers(Stream& stream, JPEGEncodingContext& context, JPEGWrit
633696
return {};
634697
}
635698

636-
ErrorOr<void> add_image(Stream& stream, JPEGEncodingContext& context, Mode mode)
699+
ErrorOr<void> add_image(Stream& stream, JPEGEncodingContext& context, JPEGWriter::Options const& options, Mode mode)
637700
{
701+
if (options.use_deringing == JPEGEncoderOptions::UseDeringing::Yes)
702+
context.apply_deringing();
638703
context.convert_to_ycbcr(mode);
639704
context.fdct_and_quantization(mode);
640705
TRY(context.write_huffman_stream(mode));
@@ -649,7 +714,7 @@ ErrorOr<void> JPEGWriter::encode(Stream& stream, Bitmap const& bitmap, Options c
649714
JPEGEncodingContext context { JPEGBigEndianOutputBitStream { stream } };
650715
TRY(add_headers(stream, context, options, bitmap.size(), Mode::RGB));
651716
TRY(context.initialize_mcu(bitmap));
652-
TRY(add_image(stream, context, Mode::RGB));
717+
TRY(add_image(stream, context, options, Mode::RGB));
653718
return {};
654719
}
655720

@@ -658,7 +723,7 @@ ErrorOr<void> JPEGWriter::encode(Stream& stream, CMYKBitmap const& bitmap, Optio
658723
JPEGEncodingContext context { JPEGBigEndianOutputBitStream { stream } };
659724
TRY(add_headers(stream, context, options, bitmap.size(), Mode::CMYK));
660725
TRY(context.initialize_mcu(bitmap));
661-
TRY(add_image(stream, context, Mode::CMYK));
726+
TRY(add_image(stream, context, options, Mode::CMYK));
662727
return {};
663728
}
664729

Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,14 @@
1212
namespace Gfx {
1313

1414
struct JPEGEncoderOptions {
15+
enum class UseDeringing : u8 {
16+
Yes,
17+
No,
18+
};
19+
1520
Optional<ReadonlyBytes> icc_data;
1621
u8 quality { 75 };
22+
UseDeringing use_deringing { UseDeringing::Yes };
1723
};
1824

1925
class JPEGWriter {

0 commit comments

Comments
 (0)