mirror of
https://github.com/RGBCube/serenity
synced 2025-07-27 06:57:45 +00:00
LibGfx: Add StackBlurFilter, an efficient almost gaussian blur
This adds an implementation of StackBlur which is a very efficient blur that closely approximates a gaussian blur. It has a number of benefits over the existing FastBoxBlurFilter: 1. It's faster (~half the pixel lookups of a single box blur pass) 2. It only requires a single pass over image to produce good results - (Rather than the 3 the box blur requires) 3. It only needs to allocate a buffer of `blur_radius * 2 + 1` colors - These easily fits on the stack for any reasonable radius For a detailed explanation of the algorithm check out this link: https://observablehq.com/@jobleonard/mario-klingemans-stackblur
This commit is contained in:
parent
072a78b958
commit
7f8cf81f7a
3 changed files with 345 additions and 6 deletions
|
@ -1,10 +1,10 @@
|
||||||
set(SOURCES
|
set(SOURCES
|
||||||
AffineTransform.cpp
|
AffineTransform.cpp
|
||||||
AntiAliasingPainter.cpp
|
AntiAliasingPainter.cpp
|
||||||
Bitmap.cpp
|
|
||||||
BitmapMixer.cpp
|
|
||||||
BMPLoader.cpp
|
BMPLoader.cpp
|
||||||
BMPWriter.cpp
|
BMPWriter.cpp
|
||||||
|
Bitmap.cpp
|
||||||
|
BitmapMixer.cpp
|
||||||
ClassicStylePainter.cpp
|
ClassicStylePainter.cpp
|
||||||
ClassicWindowTheme.cpp
|
ClassicWindowTheme.cpp
|
||||||
Color.cpp
|
Color.cpp
|
||||||
|
@ -14,27 +14,28 @@ set(SOURCES
|
||||||
Filters/ColorBlindnessFilter.cpp
|
Filters/ColorBlindnessFilter.cpp
|
||||||
Filters/FastBoxBlurFilter.cpp
|
Filters/FastBoxBlurFilter.cpp
|
||||||
Filters/LumaFilter.cpp
|
Filters/LumaFilter.cpp
|
||||||
|
Filters/StackBlurFilter.cpp
|
||||||
Font/BitmapFont.cpp
|
Font/BitmapFont.cpp
|
||||||
Font/Emoji.cpp
|
Font/Emoji.cpp
|
||||||
Font/FontDatabase.cpp
|
Font/FontDatabase.cpp
|
||||||
Font/ScaledFont.cpp
|
Font/ScaledFont.cpp
|
||||||
|
Font/TrueType/Cmap.cpp
|
||||||
Font/TrueType/Font.cpp
|
Font/TrueType/Font.cpp
|
||||||
Font/TrueType/Glyf.cpp
|
Font/TrueType/Glyf.cpp
|
||||||
Font/TrueType/Cmap.cpp
|
|
||||||
Font/Typeface.cpp
|
Font/Typeface.cpp
|
||||||
Font/WOFF/Font.cpp
|
Font/WOFF/Font.cpp
|
||||||
GIFLoader.cpp
|
GIFLoader.cpp
|
||||||
ICOLoader.cpp
|
ICOLoader.cpp
|
||||||
ImageDecoder.cpp
|
ImageDecoder.cpp
|
||||||
JPGLoader.cpp
|
JPGLoader.cpp
|
||||||
Painter.cpp
|
|
||||||
Palette.cpp
|
|
||||||
Path.cpp
|
|
||||||
PBMLoader.cpp
|
PBMLoader.cpp
|
||||||
PGMLoader.cpp
|
PGMLoader.cpp
|
||||||
PNGLoader.cpp
|
PNGLoader.cpp
|
||||||
PNGWriter.cpp
|
PNGWriter.cpp
|
||||||
PPMLoader.cpp
|
PPMLoader.cpp
|
||||||
|
Painter.cpp
|
||||||
|
Palette.cpp
|
||||||
|
Path.cpp
|
||||||
Point.cpp
|
Point.cpp
|
||||||
QOILoader.cpp
|
QOILoader.cpp
|
||||||
QOIWriter.cpp
|
QOIWriter.cpp
|
||||||
|
|
311
Userland/Libraries/LibGfx/Filters/StackBlurFilter.cpp
Normal file
311
Userland/Libraries/LibGfx/Filters/StackBlurFilter.cpp
Normal file
|
@ -0,0 +1,311 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010, Mario Klingemann <mario@quasimondo.com>
|
||||||
|
* Copyright (c) 2022, MacDue <macdue@dueutil.tech>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#if defined(__GNUC__) && !defined(__clang__)
|
||||||
|
# pragma GCC optimize("O3")
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <AK/Array.h>
|
||||||
|
#include <AK/Math.h>
|
||||||
|
#include <AK/Vector.h>
|
||||||
|
#include <LibGfx/Filters/StackBlurFilter.h>
|
||||||
|
|
||||||
|
namespace Gfx {
|
||||||
|
|
||||||
|
using uint = unsigned;
|
||||||
|
|
||||||
|
constexpr size_t MAX_RADIUS = 256;
|
||||||
|
|
||||||
|
// Magic lookup tables!
|
||||||
|
// `(value * sum_mult[radius - 2]) >> shift_table[radius - 2]` closely approximates value/(radius*radius)
|
||||||
|
// These LUTs are the same as the original, but converted to constexpr functions rather than magic numbers.
|
||||||
|
|
||||||
|
constexpr auto shift_table = [] {
|
||||||
|
Array<u8, MAX_RADIUS> lut {};
|
||||||
|
for (size_t r = 2; r <= MAX_RADIUS + 1; r++)
|
||||||
|
lut[r - 2] = static_cast<u8>(AK::ceil_log2(256 * (r * r + 1)));
|
||||||
|
return lut;
|
||||||
|
}();
|
||||||
|
|
||||||
|
constexpr auto mult_table = [] {
|
||||||
|
Array<u16, MAX_RADIUS> lut {};
|
||||||
|
for (size_t r = 2; r <= MAX_RADIUS + 1; r++)
|
||||||
|
lut[r - 2] = static_cast<u16>(AK::ceil(static_cast<double>(1 << shift_table[r - 2]) / (r * r)));
|
||||||
|
return lut;
|
||||||
|
}();
|
||||||
|
|
||||||
|
// Note: This is named to be consistent with the algorithm, but it's actually a simple circular buffer.
|
||||||
|
struct BlurStack {
|
||||||
|
BlurStack(size_t size)
|
||||||
|
{
|
||||||
|
m_data.resize(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Iterator {
|
||||||
|
friend BlurStack;
|
||||||
|
|
||||||
|
ALWAYS_INLINE Color& operator*()
|
||||||
|
{
|
||||||
|
return m_data.at(m_idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
ALWAYS_INLINE Color* operator->()
|
||||||
|
{
|
||||||
|
return &m_data.at(m_idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
ALWAYS_INLINE Iterator operator++()
|
||||||
|
{
|
||||||
|
// Note: This seemed to profile slightly better than %
|
||||||
|
if (++m_idx >= m_data.size())
|
||||||
|
m_idx = 0;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ALWAYS_INLINE Iterator operator++(int)
|
||||||
|
{
|
||||||
|
auto prev_it = *this;
|
||||||
|
++*(this);
|
||||||
|
return prev_it;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Iterator(size_t idx, Span<Color> data)
|
||||||
|
: m_idx(idx)
|
||||||
|
, m_data(data)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t m_idx;
|
||||||
|
Span<Color> m_data;
|
||||||
|
};
|
||||||
|
|
||||||
|
Iterator iterator_from_position(size_t position)
|
||||||
|
{
|
||||||
|
VERIFY(position < m_data.size());
|
||||||
|
return Iterator(position, m_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Vector<Color, 512> m_data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// This is an implementation of StackBlur by Mario Klingemann (https://observablehq.com/@jobleonard/mario-klingemans-stackblur)
|
||||||
|
// (Link is to a secondary source as the original site is now down)
|
||||||
|
FLATTEN void StackBlurFilter::process_rgba(u8 radius)
|
||||||
|
{
|
||||||
|
// TODO: Implement a plain RGB version of this (if required)
|
||||||
|
|
||||||
|
if (radius == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
constexpr auto transparent_white = Color(Color::White).with_alpha(0);
|
||||||
|
|
||||||
|
uint width = m_bitmap.width();
|
||||||
|
uint height = m_bitmap.height();
|
||||||
|
|
||||||
|
uint div = 2 * radius + 1;
|
||||||
|
uint radius_plus_1 = radius + 1;
|
||||||
|
uint sum_factor = radius_plus_1 * (radius_plus_1 + 1) / 2;
|
||||||
|
|
||||||
|
auto get_pixel = [&](int x, int y) {
|
||||||
|
auto color = m_bitmap.get_pixel<StorageFormat::BGRA8888>(x, y);
|
||||||
|
if (color.alpha() == 0)
|
||||||
|
return transparent_white;
|
||||||
|
return color;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto set_pixel = [&](int x, int y, Color color) {
|
||||||
|
return m_bitmap.set_pixel<StorageFormat::BGRA8888>(x, y, color);
|
||||||
|
};
|
||||||
|
|
||||||
|
BlurStack blur_stack { div };
|
||||||
|
auto const stack_start = blur_stack.iterator_from_position(0);
|
||||||
|
auto const stack_end = blur_stack.iterator_from_position(radius_plus_1);
|
||||||
|
auto stack_iterator = stack_start;
|
||||||
|
|
||||||
|
auto const sum_mult = mult_table[radius - 1];
|
||||||
|
auto const sum_shift = shift_table[radius - 1];
|
||||||
|
|
||||||
|
for (uint y = 0; y < height; y++) {
|
||||||
|
stack_iterator = stack_start;
|
||||||
|
|
||||||
|
auto color = get_pixel(0, y);
|
||||||
|
for (uint i = 0; i < radius_plus_1; i++)
|
||||||
|
*(stack_iterator++) = color;
|
||||||
|
|
||||||
|
// All the sums here work to approximate a gaussian.
|
||||||
|
// Note: Only about 17 bits are actually used in each sum.
|
||||||
|
uint red_in_sum = 0;
|
||||||
|
uint green_in_sum = 0;
|
||||||
|
uint blue_in_sum = 0;
|
||||||
|
uint alpha_in_sum = 0;
|
||||||
|
uint red_out_sum = radius_plus_1 * color.red();
|
||||||
|
uint green_out_sum = radius_plus_1 * color.green();
|
||||||
|
uint blue_out_sum = radius_plus_1 * color.blue();
|
||||||
|
uint alpha_out_sum = radius_plus_1 * color.alpha();
|
||||||
|
uint red_sum = sum_factor * color.red();
|
||||||
|
uint green_sum = sum_factor * color.green();
|
||||||
|
uint blue_sum = sum_factor * color.blue();
|
||||||
|
uint alpha_sum = sum_factor * color.alpha();
|
||||||
|
|
||||||
|
for (uint i = 1; i <= radius; i++) {
|
||||||
|
auto color = get_pixel(min(i, width - 1), y);
|
||||||
|
|
||||||
|
auto bias = radius_plus_1 - i;
|
||||||
|
*stack_iterator = color;
|
||||||
|
red_sum += color.red() * bias;
|
||||||
|
green_sum += color.green() * bias;
|
||||||
|
blue_sum += color.blue() * bias;
|
||||||
|
alpha_sum += color.alpha() * bias;
|
||||||
|
|
||||||
|
red_in_sum += color.red();
|
||||||
|
green_in_sum += color.green();
|
||||||
|
blue_in_sum += color.blue();
|
||||||
|
alpha_in_sum += color.alpha();
|
||||||
|
|
||||||
|
++stack_iterator;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto stack_in_iterator = stack_start;
|
||||||
|
auto stack_out_iterator = stack_end;
|
||||||
|
|
||||||
|
for (uint x = 0; x < width; x++) {
|
||||||
|
auto alpha = (alpha_sum * sum_mult) >> sum_shift;
|
||||||
|
if (alpha != 0)
|
||||||
|
set_pixel(x, y, Color((red_sum * sum_mult) >> sum_shift, (green_sum * sum_mult) >> sum_shift, (blue_sum * sum_mult) >> sum_shift, alpha));
|
||||||
|
else
|
||||||
|
set_pixel(x, y, transparent_white);
|
||||||
|
|
||||||
|
red_sum -= red_out_sum;
|
||||||
|
green_sum -= green_out_sum;
|
||||||
|
blue_sum -= blue_out_sum;
|
||||||
|
alpha_sum -= alpha_out_sum;
|
||||||
|
|
||||||
|
red_out_sum -= stack_in_iterator->red();
|
||||||
|
green_out_sum -= stack_in_iterator->green();
|
||||||
|
blue_out_sum -= stack_in_iterator->blue();
|
||||||
|
alpha_out_sum -= stack_in_iterator->alpha();
|
||||||
|
|
||||||
|
auto color = get_pixel(min(x + radius_plus_1, width - 1), y);
|
||||||
|
*stack_in_iterator = color;
|
||||||
|
red_in_sum += color.red();
|
||||||
|
green_in_sum += color.green();
|
||||||
|
blue_in_sum += color.blue();
|
||||||
|
alpha_in_sum += color.alpha();
|
||||||
|
|
||||||
|
red_sum += red_in_sum;
|
||||||
|
green_sum += green_in_sum;
|
||||||
|
blue_sum += blue_in_sum;
|
||||||
|
alpha_sum += alpha_in_sum;
|
||||||
|
|
||||||
|
++stack_in_iterator;
|
||||||
|
|
||||||
|
color = *stack_out_iterator;
|
||||||
|
red_out_sum += color.red();
|
||||||
|
green_out_sum += color.green();
|
||||||
|
blue_out_sum += color.blue();
|
||||||
|
alpha_out_sum += color.alpha();
|
||||||
|
|
||||||
|
red_in_sum -= color.red();
|
||||||
|
green_in_sum -= color.green();
|
||||||
|
blue_in_sum -= color.blue();
|
||||||
|
alpha_in_sum -= color.alpha();
|
||||||
|
|
||||||
|
++stack_out_iterator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint x = 0; x < width; x++) {
|
||||||
|
stack_iterator = stack_start;
|
||||||
|
|
||||||
|
auto color = get_pixel(x, 0);
|
||||||
|
for (uint i = 0; i < radius_plus_1; i++)
|
||||||
|
*(stack_iterator++) = color;
|
||||||
|
|
||||||
|
uint red_in_sum = 0;
|
||||||
|
uint green_in_sum = 0;
|
||||||
|
uint blue_in_sum = 0;
|
||||||
|
uint alpha_in_sum = 0;
|
||||||
|
uint red_out_sum = radius_plus_1 * color.red();
|
||||||
|
uint green_out_sum = radius_plus_1 * color.green();
|
||||||
|
uint blue_out_sum = radius_plus_1 * color.blue();
|
||||||
|
uint alpha_out_sum = radius_plus_1 * color.alpha();
|
||||||
|
uint red_sum = sum_factor * color.red();
|
||||||
|
uint green_sum = sum_factor * color.green();
|
||||||
|
uint blue_sum = sum_factor * color.blue();
|
||||||
|
uint alpha_sum = sum_factor * color.alpha();
|
||||||
|
|
||||||
|
for (uint i = 1; i <= radius; i++) {
|
||||||
|
auto color = get_pixel(x, min(i, height - 1));
|
||||||
|
|
||||||
|
auto bias = radius_plus_1 - i;
|
||||||
|
*stack_iterator = color;
|
||||||
|
red_sum += color.red() * bias;
|
||||||
|
green_sum += color.green() * bias;
|
||||||
|
blue_sum += color.blue() * bias;
|
||||||
|
alpha_sum += color.alpha() * bias;
|
||||||
|
|
||||||
|
red_in_sum += color.red();
|
||||||
|
green_in_sum += color.green();
|
||||||
|
blue_in_sum += color.blue();
|
||||||
|
alpha_in_sum += color.alpha();
|
||||||
|
|
||||||
|
++stack_iterator;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto stack_in_iterator = stack_start;
|
||||||
|
auto stack_out_iterator = stack_end;
|
||||||
|
|
||||||
|
for (uint y = 0; y < height; y++) {
|
||||||
|
auto alpha = (alpha_sum * sum_mult) >> sum_shift;
|
||||||
|
if (alpha != 0)
|
||||||
|
set_pixel(x, y, Color((red_sum * sum_mult) >> sum_shift, (green_sum * sum_mult) >> sum_shift, (blue_sum * sum_mult) >> sum_shift, alpha));
|
||||||
|
else
|
||||||
|
set_pixel(x, y, transparent_white);
|
||||||
|
|
||||||
|
red_sum -= red_out_sum;
|
||||||
|
green_sum -= green_out_sum;
|
||||||
|
blue_sum -= blue_out_sum;
|
||||||
|
alpha_sum -= alpha_out_sum;
|
||||||
|
|
||||||
|
red_out_sum -= stack_in_iterator->red();
|
||||||
|
green_out_sum -= stack_in_iterator->green();
|
||||||
|
blue_out_sum -= stack_in_iterator->blue();
|
||||||
|
alpha_out_sum -= stack_in_iterator->alpha();
|
||||||
|
|
||||||
|
auto color = get_pixel(x, min(y + radius_plus_1, height - 1));
|
||||||
|
*stack_in_iterator = color;
|
||||||
|
red_in_sum += color.red();
|
||||||
|
green_in_sum += color.green();
|
||||||
|
blue_in_sum += color.blue();
|
||||||
|
alpha_in_sum += color.alpha();
|
||||||
|
|
||||||
|
red_sum += red_in_sum;
|
||||||
|
green_sum += green_in_sum;
|
||||||
|
blue_sum += blue_in_sum;
|
||||||
|
alpha_sum += alpha_in_sum;
|
||||||
|
|
||||||
|
++stack_in_iterator;
|
||||||
|
|
||||||
|
color = *stack_out_iterator;
|
||||||
|
red_out_sum += color.red();
|
||||||
|
green_out_sum += color.green();
|
||||||
|
blue_out_sum += color.blue();
|
||||||
|
alpha_out_sum += color.alpha();
|
||||||
|
|
||||||
|
red_in_sum -= color.red();
|
||||||
|
green_in_sum -= color.green();
|
||||||
|
blue_in_sum -= color.blue();
|
||||||
|
alpha_in_sum -= color.alpha();
|
||||||
|
|
||||||
|
++stack_out_iterator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
27
Userland/Libraries/LibGfx/Filters/StackBlurFilter.h
Normal file
27
Userland/Libraries/LibGfx/Filters/StackBlurFilter.h
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022, MacDue <macdue@dueutil.tech>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <LibGfx/Bitmap.h>
|
||||||
|
|
||||||
|
namespace Gfx {
|
||||||
|
|
||||||
|
class StackBlurFilter {
|
||||||
|
public:
|
||||||
|
StackBlurFilter(Bitmap& bitmap)
|
||||||
|
: m_bitmap(bitmap)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: The radius is a u8 for reason! This implementation can only handle radii from 0 to 255.
|
||||||
|
void process_rgba(u8 radius);
|
||||||
|
|
||||||
|
private:
|
||||||
|
Bitmap& m_bitmap;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue