#include "effects/blur.hpp" #include #include #include #include "core/parallel.hpp" #include "effects/source_fill.hpp" namespace effector { const std::vector& BlurEffect::params() const { static const std::vector 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 { // 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::min(i, count - 1)) * stride + c]; } } for (int i = 0; i < count; ++i) { for (int c = 0; c < 3; ++c) { out[static_cast(i) * stride + c] = static_cast(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(add) * stride + c] - in[static_cast(drop) * stride + c]; } } } void blurHorizontal(const Image& src, Image& dst, int radius) { parallelFor(static_cast(src.height()), [&](std::size_t begin, std::size_t end) { for (std::size_t y = begin; y < end; ++y) { boxPass(src.row(static_cast(y)), dst.row(static_cast(y)), src.width(), 3, radius); } }); } void blurVertical(const Image& src, Image& dst, int radius) { const std::size_t stride = src.stride(); parallelFor(static_cast(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& 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(std::max(1L, paramInt(values, "radius", 8))); const int passes = static_cast(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(rng.inRange(0, std::max(0, src.width() - width))); const int sy = static_cast(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