Add a blur effect plugin
Fills the canvas from one source, tiling it when it is smaller, then applies a separable box blur repeated `passes` times to approximate a gaussian. Params: radius, passes, direction and first_source, so the CLI flags and the GUI widgets come out of the ParamSpecs as usual. All random values (source index and crop offset) are drawn serially before the parallel pixel work, so a seed reproduces the image exactly; the box window clamps at both ends and the radius is clamped to the canvas so an oversized radius cannot read out of bounds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
#include "core/registry.hpp"
|
||||
|
||||
#include "effects/blur.hpp"
|
||||
#include "effects/scanlines.hpp"
|
||||
|
||||
namespace effector {
|
||||
@@ -8,6 +9,7 @@ namespace effector {
|
||||
const std::vector<EffectPtr>& effects() {
|
||||
static const std::vector<EffectPtr> registry = {
|
||||
std::make_shared<ScanlinesEffect>(),
|
||||
std::make_shared<BlurEffect>(),
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
#include "effects/blur.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "core/parallel.hpp"
|
||||
|
||||
namespace effector {
|
||||
|
||||
const std::vector<ParamSpec>& BlurEffect::params() const {
|
||||
static const std::vector<ParamSpec> specs = {
|
||||
ParamSpec::makeInt("radius", "Radius", 8, 1, 256,
|
||||
"Half width in pixels of the blur window."),
|
||||
ParamSpec::makeInt("passes", "Passes", 2, 1, 8,
|
||||
"How often the box blur is repeated; more passes look more gaussian."),
|
||||
ParamSpec::makeChoice("direction", "Direction", "both", {"both", "horizontal", "vertical"},
|
||||
"Axis the blur is applied along."),
|
||||
ParamSpec::makeBool("first_source", "Use the first image", false,
|
||||
"Always blur the first source instead of picking one at random."),
|
||||
};
|
||||
return specs;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Copies `src` into the canvas starting at (sx, sy), repeating it as needed so
|
||||
// that every pixel is covered even when the source is smaller than the canvas.
|
||||
void fillFromSource(Image& canvas, const Image& src, int sx, int sy) {
|
||||
const int width = canvas.width();
|
||||
const std::size_t total = static_cast<std::size_t>(width) * 3;
|
||||
const int copyWidth = std::min(width, src.width() - sx);
|
||||
const std::size_t copyBytes = static_cast<std::size_t>(copyWidth) * 3;
|
||||
|
||||
parallelFor(static_cast<std::size_t>(canvas.height()), [&](std::size_t begin, std::size_t end) {
|
||||
for (std::size_t y = begin; y < end; ++y) {
|
||||
const int srcY = sy + (static_cast<int>(y) % std::max(1, src.height() - sy));
|
||||
const std::uint8_t* in = src.row(srcY) + static_cast<std::size_t>(sx) * 3;
|
||||
std::uint8_t* out = canvas.row(static_cast<int>(y));
|
||||
|
||||
std::memcpy(out, in, copyBytes);
|
||||
std::size_t filled = copyBytes;
|
||||
while (filled < total) {
|
||||
const std::size_t chunk = std::min(filled, total - filled);
|
||||
std::memcpy(out + filled, out, chunk);
|
||||
filled += chunk;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// One separable box blur pass. `stride` is the distance in bytes between two
|
||||
// neighbouring samples along the blurred axis, `count` how many of them there
|
||||
// are; the window is clamped at both ends.
|
||||
void boxPass(const std::uint8_t* in, std::uint8_t* out, int count, std::size_t stride, int radius) {
|
||||
const int window = 2 * radius + 1;
|
||||
int sum[3] = {0, 0, 0};
|
||||
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
sum[c] = in[c] * (radius + 1);
|
||||
for (int i = 1; i <= radius; ++i) {
|
||||
sum[c] += in[static_cast<std::size_t>(std::min(i, count - 1)) * stride + c];
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
out[static_cast<std::size_t>(i) * stride + c] =
|
||||
static_cast<std::uint8_t>(sum[c] / window);
|
||||
}
|
||||
const int add = std::min(i + radius + 1, count - 1);
|
||||
const int drop = std::max(i - radius, 0);
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
sum[c] += in[static_cast<std::size_t>(add) * stride + c] -
|
||||
in[static_cast<std::size_t>(drop) * stride + c];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void blurHorizontal(const Image& src, Image& dst, int radius) {
|
||||
parallelFor(static_cast<std::size_t>(src.height()), [&](std::size_t begin, std::size_t end) {
|
||||
for (std::size_t y = begin; y < end; ++y) {
|
||||
boxPass(src.row(static_cast<int>(y)), dst.row(static_cast<int>(y)), src.width(), 3,
|
||||
radius);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void blurVertical(const Image& src, Image& dst, int radius) {
|
||||
const std::size_t stride = src.stride();
|
||||
parallelFor(static_cast<std::size_t>(src.width()), [&](std::size_t begin, std::size_t end) {
|
||||
for (std::size_t x = begin; x < end; ++x) {
|
||||
const std::size_t offset = x * 3;
|
||||
boxPass(src.data() + offset, dst.data() + offset, src.height(), stride, radius);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Image BlurEffect::render(const std::vector<Image>& sources, int width, int height, Rng& rng,
|
||||
const ParamValues& values) const {
|
||||
if (sources.empty()) throw EffectError("No source images.");
|
||||
if (width < 1 || height < 1) throw EffectError("Canvas size must be at least 1x1 pixels.");
|
||||
|
||||
const int radius = static_cast<int>(std::max(1L, paramInt(values, "radius", 8)));
|
||||
const int passes = static_cast<int>(std::max(1L, paramInt(values, "passes", 2)));
|
||||
const std::string direction = paramChoice(values, "direction", "both");
|
||||
const bool firstSource = paramBool(values, "first_source", false);
|
||||
|
||||
// Every random value is drawn here, before any threading, so that a seed
|
||||
// always gives the same image.
|
||||
const std::size_t index = firstSource ? 0 : rng.below(sources.size() - 1);
|
||||
const Image& src = sources[index];
|
||||
const int sx = static_cast<int>(rng.inRange(0, std::max(0, src.width() - width)));
|
||||
const int sy = static_cast<int>(rng.inRange(0, std::max(0, src.height() - height)));
|
||||
|
||||
Image canvas(width, height);
|
||||
fillFromSource(canvas, src, sx, sy);
|
||||
|
||||
Image scratch(width, height);
|
||||
for (int pass = 0; pass < passes; ++pass) {
|
||||
if (direction != "vertical") {
|
||||
blurHorizontal(canvas, scratch, std::min(radius, std::max(1, width - 1)));
|
||||
std::swap(canvas, scratch);
|
||||
}
|
||||
if (direction != "horizontal") {
|
||||
blurVertical(canvas, scratch, std::min(radius, std::max(1, height - 1)));
|
||||
std::swap(canvas, scratch);
|
||||
}
|
||||
}
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
} // namespace effector
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/effect.hpp"
|
||||
|
||||
namespace effector {
|
||||
|
||||
// Fills the canvas with one of the sources and softens it with a separable box
|
||||
// blur, repeated a few times to approximate a gaussian.
|
||||
class BlurEffect : public Effect {
|
||||
public:
|
||||
std::string name() const override { return "blur"; }
|
||||
std::string title() const override { return "Blur"; }
|
||||
std::string description() const override {
|
||||
return "Fills the canvas from a randomly chosen source and softens it with a repeated "
|
||||
"box blur, horizontally, vertically or both.";
|
||||
}
|
||||
const std::vector<ParamSpec>& params() const override;
|
||||
|
||||
Image render(const std::vector<Image>& sources, int width, int height, Rng& rng,
|
||||
const ParamValues& values) const override;
|
||||
};
|
||||
|
||||
} // namespace effector
|
||||
Reference in New Issue
Block a user