Files
effector/src/effects/blur.cpp
T
SiandClaude Opus 5 4739d39a51 Let effects declare whether they need source images
Whether an effect works from the loaded photographs is now part of the
declarative plug-in contract, next to params(): Effect::needsSources()
defaults to true, and blur — which only softens what it is handed — says
no and blurs an empty canvas when given nothing.

Only the first stage of a cascade ever sees the sources, so
chainNeedsSources() just asks the first stage. Both front-ends gate on
that instead of on "are there any images": the CLI only refuses an empty
batch when the chain wants one, and the interface renders a blur-led
chain with no images loaded, saying whichever thing is actually missing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 15:49:16 +02:00

137 lines
5.5 KiB
C++

#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 (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);
Image canvas(width, height);
if (!sources.empty()) {
// 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)));
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