Compositor: add new node: Kuwahara filter #107015

Merged
Habib Gahbiche merged 22 commits from zazizizou/blender:com-kuwahara-filter-node into main 2023-06-08 16:14:51 +02:00
11 changed files with 357 additions and 53 deletions
Showing only changes of commit bd12c2bd8e - Show all commits

View File

@ -350,8 +350,10 @@ if(WITH_COMPOSITOR_CPU)
operations/COM_GaussianXBlurOperation.h
operations/COM_GaussianYBlurOperation.cc
operations/COM_GaussianYBlurOperation.h
operations/COM_KuwaharaOperation.h
operations/COM_KuwaharaOperation.cc
operations/COM_KuwaharaClassicOperation.h
operations/COM_KuwaharaClassicOperation.cc
operations/COM_KuwaharaAnisotropicOperation.h
operations/COM_KuwaharaAnisotropicOperation.cc
operations/COM_MovieClipAttributeOperation.cc
operations/COM_MovieClipAttributeOperation.h
operations/COM_MovieDistortionOperation.cc

View File

@ -1,8 +1,12 @@
/* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright 2011 Blender Foundation. */
* Copyright 2023 Blender Foundation. */
zazizizou marked this conversation as resolved Outdated

Use the current year for the new files added. This also applies to other new files in this PR.

Use the current year for the new files added. This also applies to other new files in this PR.
#include "COM_KuwaharaNode.h"
#include "COM_KuwaharaOperation.h"
#include "COM_FastGaussianBlurOperation.h"
#include "COM_KuwaharaAnisotropicOperation.h"
#include "COM_KuwaharaClassicOperation.h"
#include "COM_MathBaseOperation.h"
#include "COM_SetValueOperation.h"
namespace blender::compositor {
@ -12,13 +16,98 @@ void KuwaharaNode::convert_to_operations(NodeConverter &converter,
const bNode *node = this->get_bnode();
const NodeKuwaharaData *data = (const NodeKuwaharaData *)node->storage;
KuwaharaOperation *operation = new KuwaharaOperation();
operation->set_kernel_size(data->kernel_size);
operation->set_variation(data->variation);
switch (data->variation) {
case CMP_NODE_KUWAHARA_CLASSIC: {
KuwaharaClassicOperation *operation = new KuwaharaClassicOperation();
operation->set_kernel_size(data->kernel_size);
converter.add_operation(operation);
converter.map_input_socket(get_input_socket(0), operation->get_input_socket(0));
converter.map_output_socket(get_output_socket(0), operation->get_output_socket());
converter.add_operation(operation);
converter.map_input_socket(get_input_socket(0), operation->get_input_socket(0));
converter.map_output_socket(get_output_socket(0), operation->get_output_socket());
} break;
zazizizou marked this conversation as resolved Outdated
`break` should be inside of the block: https://wiki.blender.org/wiki/Style_Guide/C_Cpp#Operators_and_Statements
case CMP_NODE_KUWAHARA_ANISOTROPIC: {
/* Edge detection */
auto const_fact = new SetValueOperation();
const_fact->set_value(1.0f);
converter.add_operation(const_fact);
auto sobel_x = new ConvolutionFilterOperation();
sobel_x->set3x3Filter(1, 0, -1, 2, 0, -2, 1, 0, -1);
converter.add_operation(sobel_x);
converter.map_input_socket(get_input_socket(0), sobel_x->get_input_socket(0));
converter.add_link(const_fact->get_output_socket(0), sobel_x->get_input_socket(1));
auto sobel_y = new ConvolutionFilterOperation();
sobel_y->set3x3Filter(1, 2, 1, 0, 0, 0, -1, -2, -1);
converter.add_operation(sobel_y);
converter.map_input_socket(get_input_socket(0), sobel_y->get_input_socket(0));
converter.add_link(const_fact->get_output_socket(0), sobel_y->get_input_socket(1));
/* Compute intensity of edges */
auto sobel_xx = new MathMultiplyOperation();
auto sobel_yy = new MathMultiplyOperation();
auto sobel_xy = new MathMultiplyOperation();
converter.add_operation(sobel_xx);
converter.add_operation(sobel_yy);
converter.add_operation(sobel_xy);
converter.add_link(sobel_x->get_output_socket(0), sobel_xx->get_input_socket(0));
converter.add_link(sobel_x->get_output_socket(0), sobel_xx->get_input_socket(1));
converter.add_link(sobel_y->get_output_socket(0), sobel_yy->get_input_socket(0));
converter.add_link(sobel_y->get_output_socket(0), sobel_yy->get_input_socket(1));
converter.add_link(sobel_x->get_output_socket(0), sobel_xy->get_input_socket(0));
converter.add_link(sobel_y->get_output_socket(0), sobel_xy->get_input_socket(1));
/* blurring for more robustness. */
// Note: blurring doesn't make as big of a difference as I was expecting,
// especially around edges.
// Todo: investigate further and remove if necessary. For now the parameter is kept for
// better user feedback
float sigma = data->sigma;
auto blur_sobel_xx = new FastGaussianBlurOperation();
auto blur_sobel_yy = new FastGaussianBlurOperation();
auto blur_sobel_xy = new FastGaussianBlurOperation();
blur_sobel_yy->set_size(sigma, sigma);
blur_sobel_xx->set_size(sigma, sigma);
blur_sobel_xy->set_size(sigma, sigma);
converter.add_operation(blur_sobel_xx);
converter.add_operation(blur_sobel_yy);
converter.add_operation(blur_sobel_xy);
converter.add_link(sobel_xx->get_output_socket(0), blur_sobel_xx->get_input_socket(0));
converter.add_link(sobel_yy->get_output_socket(0), blur_sobel_yy->get_input_socket(0));
converter.add_link(sobel_xy->get_output_socket(0), blur_sobel_xy->get_input_socket(0));
// For now, orientation is part of kuwahara operation.
// todo: implement orientation as a separate operation
// auto orientation = new OrientationOperation(); // OrientationOperation
/* Apply anisotropic Kuwahara filter */
KuwaharaAnisotropicOperation *aniso = new KuwaharaAnisotropicOperation();
aniso->set_kernel_size(data->kernel_size);
converter.map_input_socket(get_input_socket(0), aniso->get_input_socket(0));
converter.add_operation(aniso);
converter.add_link(blur_sobel_xx->get_output_socket(0), aniso->get_input_socket(1));
converter.add_link(blur_sobel_yy->get_output_socket(0), aniso->get_input_socket(2));
converter.add_link(blur_sobel_xy->get_output_socket(0), aniso->get_input_socket(3));
converter.map_output_socket(get_output_socket(0), aniso->get_output_socket(0));
// For debug. Todo: remove
// converter.map_output_socket(get_output_socket(1), sobel_xx->get_output_socket(0));
// converter.map_output_socket(get_output_socket(2), blur_sobel_xx->get_output_socket(0));
// converter.map_output_socket(get_output_socket(3), blur_sobel_xy->get_output_socket(0));
break;
}
}
}
} // namespace blender::compositor

View File

@ -10,6 +10,7 @@ namespace blender::compositor {
FastGaussianBlurOperation::FastGaussianBlurOperation() : BlurBaseOperation(DataType::Color)
{
iirgaus_ = nullptr;
data_.filtertype = R_FILTER_FAST_GAUSS;
}
void FastGaussianBlurOperation::execute_pixel(float output[4], int x, int y, void *data)
@ -67,6 +68,15 @@ void FastGaussianBlurOperation::deinit_execution()
BlurBaseOperation::deinit_mutex();
}
void FastGaussianBlurOperation::set_size(int size_x, int size_y)
{
// todo: there should be a better way to use the operation without knowing specifics of the blur
zazizizou marked this conversation as resolved Outdated

We use C style comments, even in C++: /* ... */

We use C style comments, even in C++: `/* ... */`
// node (data_) Could use factory pattern to solve this problem.
data_.sizex = size_x;
data_.sizey = size_y;
sizeavailable_ = true;
}
void *FastGaussianBlurOperation::initialize_tile_data(rcti *rect)
{
lock_mutex();

View File

@ -27,6 +27,8 @@ class FastGaussianBlurOperation : public BlurBaseOperation {
void deinit_execution() override;
void init_execution() override;
void set_size(int size_x, int size_y);
void get_area_of_interest(int input_idx, const rcti &output_area, rcti &r_input_area) override;
void update_memory_buffer_started(MemoryBuffer *output,
const rcti &area,

View File

@ -0,0 +1,166 @@
/* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright 2023 Blender Foundation. */
#include "COM_KuwaharaAnisotropicOperation.h"
#include "BLI_vector.hh"
namespace blender::compositor {
KuwaharaAnisotropicOperation::KuwaharaAnisotropicOperation()
{
this->add_input_socket(DataType::Color);
this->add_input_socket(DataType::Color);
this->add_input_socket(DataType::Color);
this->add_input_socket(DataType::Color);
this->add_output_socket(DataType::Color);
// output for debug
// todo: remove
this->add_output_socket(DataType::Color);
this->add_output_socket(DataType::Color);
this->add_output_socket(DataType::Color);
zazizizou marked this conversation as resolved Outdated

Is it something you planned to do before the actual merge?

Is it something you planned to do before the actual merge?
this->set_kernel_size(8);
this->flags_.is_fullframe_operation = true;
}
void KuwaharaAnisotropicOperation::init_execution()
{
image_reader_ = this->get_input_socket_reader(0);
}
void KuwaharaAnisotropicOperation::deinit_execution()
{
image_reader_ = nullptr;
}
void KuwaharaAnisotropicOperation::execute_pixel_sampled(float output[4],
float x,
float y,
PixelSampler sampler)
{
/* Not implemented */
}
void KuwaharaAnisotropicOperation::set_kernel_size(int kernel_size)
{
zazizizou marked this conversation as resolved Outdated

Should we consider something from:

  • Pass-through the input as-is
  • Output magenta

So that if someone saves file with the Kuwahara node in it and someone opens it without enabling full-frame compositor we do not leave the output uninitialized?

Should we consider something from: - Pass-through the input as-is - Output magenta So that if someone saves file with the Kuwahara node in it and someone opens it without enabling full-frame compositor we do not leave the output uninitialized?
kernel_size_ = kernel_size;
}
int KuwaharaAnisotropicOperation::get_kernel_size()
{
return kernel_size_;
}
void KuwaharaAnisotropicOperation::update_memory_buffer_partial(MemoryBuffer *output,
const rcti &area,
Span<MemoryBuffer *> inputs)
{
/*
Implementation based on Kyprianidis, Jan & Kang, Henry & Döllner, Jürgen. (2009).
"Image and Video Abstraction by Anisotropic Kuwahara Filtering".
Comput. Graph. Forum. 28. 1955-1963. 10.1111/j.1467-8659.2009.01574.x.
Used reference implementation from lime image processing library (MIT license).
*/
MemoryBuffer *image = inputs[0];
MemoryBuffer *s_xx = inputs[1];
MemoryBuffer *s_yy = inputs[2];
MemoryBuffer *s_xy = inputs[3];
// BLI_assert all inputs have same size
int n_div = 8; // recommended by authors in original paper
double angle = 2.0 * M_PI / n_div;
double q = 3.0;
const float EPS = 1.0e-10;
for (BuffersIterator<float> it = output->iterate_with(inputs, area); !it.is_end(); ++it) {
const int x = it.x;
const int y = it.y;
/* Compute orientation */
// todo: make orientation separate operation
// for now use green channel to compute orientation
// todo: convert to HSV and compute orientation and strength on luminance channel
const float a = s_xx->get_value(x, y, 1);
const float b = s_xy->get_value(x, y, 1);
const float c = s_yy->get_value(x, y, 1);
/* Compute egenvalues of structure tensor */
const double tr = a + c;
const double discr = sqrt((a - b)*(a-b) + 4 * b * c);
const double lambda1 = (tr + discr) / 2;
const double lambda2 = (tr - discr) / 2;
/* Compute orientation and its strength based on structure tensor */
const double orientation = 0.5 * atan2(2 * b, a - c);
const double strength = (lambda1 == 0 && lambda2 == 0) ? 0 : (lambda1 - lambda2) / (lambda1 + lambda2);
for(int ch = 0; ch < 3; ch++) {
// todo: compute anisotropy and weights on luminance channel to avoid color artifacts
zazizizou marked this conversation as resolved Outdated

It might help making it an explicit const int height = this->get_height() outside of the loop. I am not sure compiler is smart enough to do it for us.
Although, also not suer it will give measurable time impact, but still feels like a good thing to do.

It might help making it an explicit `const int height = this->get_height()` outside of the loop. I am not sure compiler is smart enough to do it for us. Although, also not suer it will give measurable time impact, but still feels like a good thing to do.
Vector<float> sum(n_div, 0.0f);
Vector<float> var(n_div, 0.0f);
Vector<float> weight(n_div, 0.0f);
float sx = 1.0f / (strength + 1.0f);
float sy = (1.0f + strength) / 1.0f;
float theta = -orientation;
for (int dy = -kernel_size_; dy <= kernel_size_; dy++) {
for (int dx = -kernel_size_; dx <= kernel_size_; dx++) {
if (dx == 0 && dy == 0) continue;
zazizizou marked this conversation as resolved Outdated

For the arithmetic types you can do int(foo) https://wiki.blender.org/wiki/Style_Guide/C_Cpp#C.2B.2B_Type_Cast

Probably will make code a bit easier to read.

For the arithmetic types you can do `int(foo)` https://wiki.blender.org/wiki/Style_Guide/C_Cpp#C.2B.2B_Type_Cast Probably will make code a bit easier to read.
// rotate and scale the kernel. This is the "anisotropic" part.
int dx2 = static_cast<int>(sx * (cos(theta) * dx - sin(theta) * dy));
int dy2 = static_cast<int>(sy * (sin(theta) * dx + cos(theta) * dy));
int xx = x + dx2;
int yy = y + dy2;
if (xx >= 0 && yy >= 0 && xx < image->get_width() && yy < image->get_height()) {
float ddx2 = (float)dx2;
float ddy2 = (float)dy2;
float theta = atan2(ddy2, ddx2) + M_PI;
int t = static_cast<int>(floor(theta / angle)) % n_div;
float d2 = dx2 * dx2 + dy2 * dy2;
float g = exp(-d2 / (2.0 * kernel_size_));
float v = image->get_value(xx, yy, ch);
sum[t] += g * v;
var[t] += g * v * v;
weight[t] += g;
}
}
zazizizou marked this conversation as resolved
Review

In other performance critical areas we do

const float weight_inv = 1.0f / weight;
a = ... foo * weight_inv ..;
b = ... bar * weight_inv ..;
c = ... baz * weight_inv ..;

Again, not something which i know for sure will show performance impact, but might worth doing so nevertheless.

In other performance critical areas we do ``` const float weight_inv = 1.0f / weight; a = ... foo * weight_inv ..; b = ... bar * weight_inv ..; c = ... baz * weight_inv ..; ``` Again, not something which i know for sure will show performance impact, but might worth doing so nevertheless.
}
// Calculate weighted average
float de = 0.0;
float nu = 0.0;
for (int i = 0; i < n_div; i++) {
sum[i] = weight[i] != 0 ? sum[i] / weight[i] : 0.0;
var[i] = weight[i] != 0 ? var[i] / weight[i] : 0.0;
var[i] = var[i] - sum[i] * sum[i];
var[i] = var[i] > EPS ? sqrt(var[i]) : EPS;
float w = powf(var[i], -q);
de += sum[i] * w;
nu += w;
}
float val = nu > EPS ? de / nu : 0.0;
CLAMP_MAX(val, 1.0f);
it.out[ch] = val;
}
/* No changes for alpha channel */
it.out[3] = image->get_value(x, y, 3);
}
}
} // namespace blender::compositor

View File

@ -0,0 +1,30 @@
/* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright 2023 Blender Foundation. */
#pragma once
#include "COM_MultiThreadedOperation.h"
namespace blender::compositor {
class KuwaharaAnisotropicOperation : public MultiThreadedOperation {
SocketReader *image_reader_;
int kernel_size_;
public:
KuwaharaAnisotropicOperation();
void init_execution() override;
void deinit_execution() override;
void execute_pixel_sampled(float output[4], float x, float y, PixelSampler sampler) override;
void set_kernel_size(int kernel_size);
int get_kernel_size();
void update_memory_buffer_partial(MemoryBuffer *output,
const rcti &area,
Span<MemoryBuffer *> inputs) override;
};
} // namespace blender::compositor

View File

@ -1,66 +1,50 @@
/* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright 2011 Blender Foundation. */
* Copyright 2023 Blender Foundation. */
#include "COM_KuwaharaOperation.h"
#include "COM_KuwaharaClassicOperation.h"
namespace blender::compositor {
zazizizou marked this conversation as resolved Outdated

No need to `extern "C"`` here, the header does it already.

Generally, we should not be adding such statements around #include statements, as it makes it very easy to break things when things move to C++. Some headers might need the linking specializer, and for those it is to be changed in the header itself.

No need to `extern "C"`` here, the header does it already. Generally, we should not be adding such statements around `#include` statements, as it makes it very easy to break things when things move to C++. Some headers might need the linking specializer, and for those it is to be changed in the header itself.
KuwaharaOperation::KuwaharaOperation()
KuwaharaClassicOperation::KuwaharaClassicOperation()
{
this->add_input_socket(DataType::Color);
this->add_output_socket(DataType::Color);
this->set_kernel_size(4.4f);
this->set_kernel_size(4);
this->flags_.is_fullframe_operation = true;
}
void KuwaharaOperation::init_execution()
void KuwaharaClassicOperation::init_execution()
{
image_reader_ = this->get_input_socket_reader(0);
}
void KuwaharaOperation::deinit_execution()
void KuwaharaClassicOperation::deinit_execution()
{
image_reader_ = nullptr;
}
void KuwaharaOperation::execute_pixel_sampled(float output[4],
float x,
float y,
PixelSampler sampler)
void KuwaharaClassicOperation::execute_pixel_sampled(float output[4],
float x,
float y,
PixelSampler sampler)
{
float input_value[4];
image_reader_->read_sampled(input_value, x, y, sampler);
output[0] = input_value[0] + 1.0;
output[1] = input_value[1] + 2.0;
output[2] = input_value[2] + 3.0;
output[3] = input_value[3] + 4.0;
/* Not implemented */
}
void KuwaharaOperation::set_kernel_size(int kernel_size)
void KuwaharaClassicOperation::set_kernel_size(int kernel_size)
{
zazizizou marked this conversation as resolved Outdated

Same as above.

Same as above.
kernel_size_ = kernel_size;
}
int KuwaharaOperation::get_kernel_size()
int KuwaharaClassicOperation::get_kernel_size()
{
return kernel_size_;
}
void KuwaharaOperation::set_variation(int variation)
{
variation_ = variation;
}
int KuwaharaOperation::get_variation()
{
return variation_;
}
void KuwaharaOperation::update_memory_buffer_partial(MemoryBuffer *output,
const rcti &area,
Span<MemoryBuffer *> inputs)
void KuwaharaClassicOperation::update_memory_buffer_partial(MemoryBuffer *output,
const rcti &area,
Span<MemoryBuffer *> inputs)
{
MemoryBuffer *image = inputs[0];
@ -74,13 +58,13 @@ void KuwaharaOperation::update_memory_buffer_partial(MemoryBuffer *output,
float var[4] = {0.0f, 0.0f, 0.0f, 0.0f};
int cnt[4] = {0, 0, 0, 0};
/* Split surroundings of */
/* Split surroundings of pixel into 4 overlapping regions */
for (int dy = -kernel_size_; dy <= kernel_size_; dy++) {
for (int dx = -kernel_size_; dx <= kernel_size_; dx++) {
int xx = x + dx;
int yy = y + dy;
if (xx >= 0 && yy >= 0 && xx < area.xmax && yy < area.ymax) {
if (xx >= 0 && yy >= 0 && xx < image->get_width() && yy < image->get_height()) {
float v;
v = image->get_value(xx, yy, ch);

View File

@ -1,5 +1,5 @@
/* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright 2011 Blender Foundation. */
* Copyright 2023 Blender Foundation. */
#pragma once
@ -7,14 +7,13 @@
namespace blender::compositor {
class KuwaharaOperation : public MultiThreadedOperation {
class KuwaharaClassicOperation : public MultiThreadedOperation {
SocketReader *image_reader_;
int kernel_size_;
int variation_;
public:
KuwaharaOperation();
KuwaharaClassicOperation();
void init_execution() override;
void deinit_execution() override;
@ -23,9 +22,6 @@ class KuwaharaOperation : public MultiThreadedOperation {
void set_kernel_size(int kernel_size);
int get_kernel_size();
void set_variation(int variation);
int get_variation();
void update_memory_buffer_partial(MemoryBuffer *output,
const rcti &area,
Span<MemoryBuffer *> inputs) override;

View File

@ -879,7 +879,7 @@ typedef struct NodeBilateralBlurData {
typedef struct NodeKuwaharaData {
short kernel_size;
short variation;
char _pad[4];
float sigma;
} NodeKuwaharaData;
typedef struct NodeAntiAliasingData {
@ -2102,6 +2102,12 @@ typedef enum CMPNodeGlareType {
CMP_NODE_GLARE_GHOST = 3,
} CMPNodeGlareType;
/* Kuwahara Node. Stored in variation */
typedef enum CMPNodeKuwahara {
CMP_NODE_KUWAHARA_CLASSIC = 0,
CMP_NODE_KUWAHARA_ANISOTROPIC = 1,
} CMPNodeKuwahara;
/* Plane track deform node. */
enum {

View File

@ -9265,6 +9265,14 @@ static void def_cmp_kuwahara(StructRNA *srna)
RNA_def_property_enum_items(prop, variation_items);
RNA_def_property_ui_text(prop, "", "Variation of Kuwahara filter to use.");
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update");
prop = RNA_def_property(srna, "sigma", PROP_FLOAT, PROP_NONE);
RNA_def_property_float_sdna(prop, NULL, "sigma");
RNA_def_property_ui_text(
prop,
"Sigma",
"Edges get smoothed before applying filter. Sigma controls smoothing degree.");
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update");
}
static void def_cmp_antialiasing(StructRNA *srna)

View File

@ -1,5 +1,5 @@
/* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright 2020 Blender Foundation */
* Copyright 2023 Blender Foundation */
/** \file
* \ingroup cmpnodes
@ -22,6 +22,11 @@ static void cmp_node_kuwahara_declare(NodeDeclarationBuilder &b)
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.compositor_domain_priority(0);
b.add_output<decl::Color>(N_("Image"));
// For debug. Todo:remove
// b.add_output<decl::Color>(N_("Sobel x"));
// b.add_output<decl::Color>(N_("Sobel xx blurred"));
// b.add_output<decl::Color>(N_("Sobel xy blurred"));
}
static void node_composit_init_kuwahara(bNodeTree * /*ntree*/, bNode *node)
@ -40,6 +45,12 @@ static void node_composit_buts_kuwahara(uiLayout *layout, bContext * /*C*/, Poin
uiItemR(col, ptr, "variation", 0, nullptr, ICON_NONE);
uiItemR(col, ptr, "kernel_size", 0, nullptr, ICON_NONE);
const int variation = RNA_enum_get(ptr, "variation");
if(variation == CMP_NODE_KUWAHARA_ANISOTROPIC) {
uiItemR(col, ptr, "sigma", 0, nullptr, ICON_NONE);
}
}
} // namespace blender::nodes::node_composite_kuwahara_cc