Skip to content

Commit 4e2cd96

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 4e2cd96

File tree

2 files changed

+75
-3
lines changed

2 files changed

+75
-3
lines changed

Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.cpp

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <AK/Function.h>
1313
#include <LibGfx/Bitmap.h>
1414
#include <LibGfx/CMYKBitmap.h>
15+
#include <LibGfx/Vector2.h>
1516

1617
namespace Gfx {
1718

@@ -98,6 +99,40 @@ class JPEGBigEndianOutputBitStream : public Stream {
9899
size_t m_bit_offset { 0 };
99100
};
100101

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

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

636-
ErrorOr<void> add_image(Stream& stream, JPEGEncodingContext& context, Mode mode)
700+
ErrorOr<void> add_image(Stream& stream, JPEGEncodingContext& context, JPEGWriter::Options const& options, Mode mode)
637701
{
702+
if (options.use_deringing == JPEGEncoderOptions::UseDeringing::Yes)
703+
context.apply_deringing();
638704
context.convert_to_ycbcr(mode);
639705
context.fdct_and_quantization(mode);
640706
TRY(context.write_huffman_stream(mode));
@@ -649,7 +715,7 @@ ErrorOr<void> JPEGWriter::encode(Stream& stream, Bitmap const& bitmap, Options c
649715
JPEGEncodingContext context { JPEGBigEndianOutputBitStream { stream } };
650716
TRY(add_headers(stream, context, options, bitmap.size(), Mode::RGB));
651717
TRY(context.initialize_mcu(bitmap));
652-
TRY(add_image(stream, context, Mode::RGB));
718+
TRY(add_image(stream, context, options, Mode::RGB));
653719
return {};
654720
}
655721

@@ -658,7 +724,7 @@ ErrorOr<void> JPEGWriter::encode(Stream& stream, CMYKBitmap const& bitmap, Optio
658724
JPEGEncodingContext context { JPEGBigEndianOutputBitStream { stream } };
659725
TRY(add_headers(stream, context, options, bitmap.size(), Mode::CMYK));
660726
TRY(context.initialize_mcu(bitmap));
661-
TRY(add_image(stream, context, Mode::CMYK));
727+
TRY(add_image(stream, context, options, Mode::CMYK));
662728
return {};
663729
}
664730

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)