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
19 changed files with 851 additions and 0 deletions

View File

@ -335,6 +335,7 @@ compositor_node_categories = [
NodeItem("CompositorNodeSunBeams"),
NodeItem("CompositorNodeDenoise"),
NodeItem("CompositorNodeAntiAliasing"),
NodeItem("CompositorNodeKuwahara"),
]),
CompositorNodeCategory("CMP_OP_VECTOR", "Vector", items=[
NodeItem("CompositorNodeNormal"),

View File

@ -1061,6 +1061,7 @@ void BKE_nodetree_remove_layer_n(struct bNodeTree *ntree, struct Scene *scene, i
#define CMP_NODE_INPAINT 272
#define CMP_NODE_DESPECKLE 273
#define CMP_NODE_ANTIALIASING 274
#define CMP_NODE_KUWAHARA 275
#define CMP_NODE_GLARE 301
#define CMP_NODE_TONEMAP 302

View File

@ -322,6 +322,8 @@ if(WITH_COMPOSITOR_CPU)
nodes/COM_FilterNode.h
nodes/COM_InpaintNode.cc
nodes/COM_InpaintNode.h
nodes/COM_KuwaharaNode.h
nodes/COM_KuwaharaNode.cc
nodes/COM_PosterizeNode.cc
nodes/COM_PosterizeNode.h
@ -349,6 +351,10 @@ if(WITH_COMPOSITOR_CPU)
operations/COM_GaussianXBlurOperation.h
operations/COM_GaussianYBlurOperation.cc
operations/COM_GaussianYBlurOperation.h
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

@ -62,6 +62,7 @@
#include "COM_InvertNode.h"
#include "COM_KeyingNode.h"
#include "COM_KeyingScreenNode.h"
#include "COM_KuwaharaNode.h"
#include "COM_LensDistortionNode.h"
#include "COM_LuminanceMatteNode.h"
#include "COM_MapRangeNode.h"
@ -436,6 +437,9 @@ Node *COM_convert_bnode(bNode *b_node)
case CMP_NODE_COMBINE_XYZ:
node = new CombineXYZNode(b_node);
break;
case CMP_NODE_KUWAHARA:
node = new KuwaharaNode(b_node);
break;
}
return node;
}

View File

@ -0,0 +1,103 @@
/* SPDX-FileCopyrightText: 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.
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "COM_KuwaharaNode.h"
#include "COM_FastGaussianBlurOperation.h"
#include "COM_KuwaharaAnisotropicOperation.h"
#include "COM_KuwaharaClassicOperation.h"
#include "COM_MathBaseOperation.h"
#include "COM_SetValueOperation.h"
namespace blender::compositor {
void KuwaharaNode::convert_to_operations(NodeConverter &converter,
const CompositorContext & /*context*/) const
{
const bNode *node = this->get_bnode();
const NodeKuwaharaData *data = (const NodeKuwaharaData *)node->storage;
switch (data->variation) {
case CMP_NODE_KUWAHARA_CLASSIC: {
KuwaharaClassicOperation *operation = new KuwaharaClassicOperation();
operation->set_kernel_size(data->size);
converter.add_operation(operation);
converter.map_input_socket(get_input_socket(0), operation->get_input_socket(0));
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
converter.map_output_socket(get_output_socket(0), operation->get_output_socket());
break;
}
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. */
const int sigma = data->smoothing;
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));
/* Apply anisotropic Kuwahara filter. */
KuwaharaAnisotropicOperation *aniso = new KuwaharaAnisotropicOperation();
aniso->set_kernel_size(data->size + 4);
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));
break;
}
}
}
} // namespace blender::compositor

View File

@ -0,0 +1,23 @@
/* SPDX-FileCopyrightText: 2023 Blender Foundation
*
zazizizou marked this conversation as resolved Outdated

2023

2023
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "COM_Node.h"
namespace blender::compositor {
/**
* \brief KuwaharaNode
* \ingroup Node
*/
class KuwaharaNode : public Node {
public:
KuwaharaNode(bNode *editor_node) : Node(editor_node) {}
void convert_to_operations(NodeConverter &converter,
const CompositorContext &context) const override;
};
} // namespace blender::compositor

View File

@ -11,6 +11,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)
@ -68,6 +69,15 @@ void FastGaussianBlurOperation::deinit_execution()
BlurBaseOperation::deinit_mutex();
}
void FastGaussianBlurOperation::set_size(int size_x, int size_y)
{
zazizizou marked this conversation as resolved Outdated

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

We use C style comments, even in C++: `/* ... */`
/* TODO: there should be a better way to use the operation without knowing specifics of the blur
* node (i.e. data_). We 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

@ -28,6 +28,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,295 @@
/* SPDX-FileCopyrightText: 2023 Blender Foundation
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "COM_KuwaharaAnisotropicOperation.h"
#include "BLI_math_base.hh"
#include "BLI_vector.hh"
#include "IMB_colormanagement.h"
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);
this->n_div_ = 8;
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(5);
this->flags_.is_fullframe_operation = true;
}
void KuwaharaAnisotropicOperation::init_execution()
{
image_reader_ = this->get_input_socket_reader(0);
s_xx_reader_ = this->get_input_socket_reader(1);
s_yy_reader_ = this->get_input_socket_reader(2);
s_xy_reader_ = this->get_input_socket_reader(3);
}
void KuwaharaAnisotropicOperation::deinit_execution()
{
image_reader_ = nullptr;
}
void KuwaharaAnisotropicOperation::execute_pixel_sampled(float output[4],
float x,
float y,
PixelSampler sampler)
{
const int width = this->get_width();
const int height = this->get_height();
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?
BLI_assert(width == s_xx_reader_->get_width());
BLI_assert(height == s_xx_reader_->get_height());
BLI_assert(width == s_yy_reader_->get_width());
BLI_assert(height == s_yy_reader_->get_height());
BLI_assert(width == s_xy_reader_->get_width());
BLI_assert(height == s_xy_reader_->get_height());
/* Values recommended by authors in original paper. */
const float angle = 2.0 * M_PI / n_div_;
const float q = 3.0;
const float EPS = 1.0e-10;
/* For now use green channel to compute orientation. */
/* TODO: convert to HSV and compute orientation and strength on luminance channel */
float tmp[4];
s_xx_reader_->read(tmp, x, y, nullptr);
const float a = tmp[1];
s_xy_reader_->read(tmp, x, y, nullptr);
const float b = tmp[1];
s_yy_reader_->read(tmp, x, y, nullptr);
const float c = tmp[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);
Vector<double> mean(n_div_);
Vector<double> sum(n_div_);
Vector<double> var(n_div_);
Vector<double> weight(n_div_);
for (int ch = 0; ch < 3; ch++) {
mean.fill(0.0);
sum.fill(0.0);
var.fill(0.0);
weight.fill(0.0);
double sx = 1.0f / (strength + 1.0f);
double sy = (1.0f + strength) / 1.0f;
double 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;
/* Rotate and scale the kernel. This is the "anisotropic" part. */
int dx2 = int(sx * (cos(theta) * dx - sin(theta) * dy));
int dy2 = int(sy * (sin(theta) * dx + cos(theta) * dy));
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.
/* Clamp image to avoid artefacts at borders. */
const int xx = math::clamp(int(x) + dx2, 0, width - 1);
const int yy = math::clamp(int(y) + dy2, 0, height - 1);
const double ddx2 = double(dx2);
const double ddy2 = double(dy2);
const double theta = atan2(ddy2, ddx2) + M_PI;
const int t = int(floor(theta / angle)) % n_div_;
double d2 = dx2 * dx2 + dy2 * dy2;
double g = exp(-d2 / (2.0 * kernel_size_));
float color[4];
image_reader_->read(color, xx, yy, nullptr);
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.
const double v = color[ch];
/* todo(zazizizou): only compute lum once per region */
const float lum = IMB_colormanagement_get_luminance(color);
/* todo(zazizizou): only compute mean for the selected region */
mean[t] += g * v;
sum[t] += g * lum;
var[t] += g * lum * lum;
weight[t] += g;
}
}
/* Calculate weighted average */
double de = 0.0;
double nu = 0.0;
for (int i = 0; i < n_div_; i++) {
double weight_inv = 1.0 / weight[i];
mean[i] = weight[i] != 0 ? mean[i] * weight_inv : 0.0;
sum[i] = weight[i] != 0 ? sum[i] * weight_inv : 0.0;
var[i] = weight[i] != 0 ? var[i] * weight_inv : 0.0;
var[i] = var[i] - sum[i] * sum[i];
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.
var[i] = var[i] > FLT_EPSILON ? sqrt(var[i]) : FLT_EPSILON;
double w = powf(var[i], -q);
de += mean[i] * w;
nu += w;
}
double val = nu > EPS ? de / nu : 0.0;
output[ch] = val;
}
/* No changes for alpha channel. */
image_reader_->read_sampled(tmp, x, y, sampler);
output[3] = tmp[3];
}
void KuwaharaAnisotropicOperation::set_kernel_size(int kernel_size)
{
/* Filter will be split into n_div.
* Add n_div / 2 to avoid artefacts such as random black pixels in image. */
kernel_size_ = kernel_size + n_div_ / 2;
}
int KuwaharaAnisotropicOperation::get_kernel_size()
{
return kernel_size_;
}
int KuwaharaAnisotropicOperation::get_n_div()
{
return n_div_;
}
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];
const int width = image->get_width();
const int height = image->get_height();
BLI_assert(width == s_xx->get_width());
BLI_assert(height == s_xx->get_height());
BLI_assert(width == s_yy->get_width());
BLI_assert(height == s_yy->get_height());
BLI_assert(width == s_xy->get_width());
BLI_assert(height == s_xy->get_height());
/* Values recommended by authors in original paper. */
const float angle = 2.0 * M_PI / n_div_;
const float 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;
/* 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);
Vector<double> mean(n_div_);
Vector<double> sum(n_div_);
Vector<double> var(n_div_);
Vector<double> weight(n_div_);
for (int ch = 0; ch < 3; ch++) {
mean.fill(0.0);
sum.fill(0.0);
var.fill(0.0);
weight.fill(0.0);
double sx = 1.0f / (strength + 1.0f);
zazizizou marked this conversation as resolved
Review

I've pretty much skipped the tiled implementation, but same note can be applied there.

I think we should move allocations to as high level as possible.

I've pretty much skipped the tiled implementation, but same note can be applied there. I think we should move allocations to as high level as possible.
double sy = (1.0f + strength) / 1.0f;
double 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;
/* Rotate and scale the kernel. This is the "anisotropic" part. */
int dx2 = int(sx * (cos(theta) * dx - sin(theta) * dy));
int dy2 = int(sy * (sin(theta) * dx + cos(theta) * dy));
/* Clamp image to avoid artefacts at borders. */
const int xx = math::clamp(x + dx2, 0, width - 1);
const int yy = math::clamp(y + dy2, 0, height - 1);
const double ddx2 = double(dx2);
const double ddy2 = double(dy2);
const double theta = atan2(ddy2, ddx2) + M_PI;
const int t = int(floor(theta / angle)) % n_div_;
zazizizou marked this conversation as resolved Outdated

Think this can be shortened to:

const int xx = math::clamp(x + dx2, 0, width - 1);
const int yy = math::clamp(y + dy2, 0, height - 1);

(add #include BLI_math_base.h at the top of the file).

Think this can be shortened to: ``` const int xx = math::clamp(x + dx2, 0, width - 1); const int yy = math::clamp(y + dy2, 0, height - 1); ``` (add `#include BLI_math_base.h` at the top of the file).
double d2 = dx2 * dx2 + dy2 * dy2;
double g = exp(-d2 / (2.0 * kernel_size_));
const double v = image->get_value(xx, yy, ch);
float color[4];
image->read_elem(xx, yy, color);
/* TODO(zazizizou): only compute lum once per region. */
const float lum = IMB_colormanagement_get_luminance(color);
/* TODO(zazizizou): only compute mean for the selected region. */
mean[t] += g * v;
sum[t] += g * lum;
var[t] += g * lum * lum;
weight[t] += g;
}
}
/* Calculate weighted average. */
double de = 0.0;
double nu = 0.0;
for (int i = 0; i < n_div_; i++) {
double weight_inv = 1.0 / weight[i];
mean[i] = weight[i] != 0 ? mean[i] * weight_inv : 0.0;
sum[i] = weight[i] != 0 ? sum[i] * weight_inv : 0.0;
var[i] = weight[i] != 0 ? var[i] * weight_inv : 0.0;
var[i] = var[i] - sum[i] * sum[i];
var[i] = var[i] > FLT_EPSILON ? sqrt(var[i]) : FLT_EPSILON;
double w = powf(var[i], -q);
de += mean[i] * w;
nu += w;
}
double val = nu > EPS ? de / nu : 0.0;
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,36 @@
/* SPDX-FileCopyrightText: 2023 Blender Foundation
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "COM_MultiThreadedOperation.h"
namespace blender::compositor {
class KuwaharaAnisotropicOperation : public MultiThreadedOperation {
SocketReader *image_reader_;
SocketReader *s_xx_reader_;
SocketReader *s_yy_reader_;
SocketReader *s_xy_reader_;
int kernel_size_;
int n_div_;
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();
int get_n_div();
void update_memory_buffer_partial(MemoryBuffer *output,
const rcti &area,
Span<MemoryBuffer *> inputs) override;
};
} // namespace blender::compositor

View File

@ -0,0 +1,199 @@
/* SPDX-FileCopyrightText: 2023 Blender Foundation
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "COM_KuwaharaClassicOperation.h"
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.
#include "IMB_colormanagement.h"
namespace blender::compositor {
KuwaharaClassicOperation::KuwaharaClassicOperation()
{
this->add_input_socket(DataType::Color);
this->add_output_socket(DataType::Color);
this->set_kernel_size(4);
this->flags_.is_fullframe_operation = true;
}
void KuwaharaClassicOperation::init_execution()
{
image_reader_ = this->get_input_socket_reader(0);
}
void KuwaharaClassicOperation::deinit_execution()
{
image_reader_ = nullptr;
}
void KuwaharaClassicOperation::execute_pixel_sampled(float output[4],
float x,
float y,
PixelSampler sampler)
{
for (int ch = 0; ch < 3; ch++) {
float sum[4] = {0.0f, 0.0f, 0.0f, 0.0f};
zazizizou marked this conversation as resolved Outdated

Same as above.

Same as above.
float mean[4] = {0.0f, 0.0f, 0.0f, 0.0f};
float var[4] = {0.0f, 0.0f, 0.0f, 0.0f};
int cnt[4] = {0, 0, 0, 0};
/* 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 < this->get_width() && yy < this->get_height()) {
float color[4];
image_reader_->read_sampled(color, xx, yy, sampler);
const float v = color[ch];
const float lum = IMB_colormanagement_get_luminance(color);
if (dx <= 0 && dy <= 0) {
mean[0] += v;
sum[0] += lum;
var[0] += lum * lum;
cnt[0]++;
}
if (dx >= 0 && dy <= 0) {
mean[1] += v;
sum[1] += lum;
var[1] += lum * lum;
cnt[1]++;
}
if (dx <= 0 && dy >= 0) {
mean[2] += v;
sum[2] += lum;
var[2] += lum * lum;
cnt[2]++;
}
if (dx >= 0 && dy >= 0) {
mean[3] += v;
sum[3] += lum;
var[3] += lum * lum;
cnt[3]++;
}
}
}
}
/* Compute region variances. */
for (int i = 0; i < 4; i++) {
mean[i] = cnt[i] != 0 ? mean[i] / cnt[i] : 0.0f;
sum[i] = cnt[i] != 0 ? sum[i] / cnt[i] : 0.0f;
var[i] = cnt[i] != 0 ? var[i] / cnt[i] : 0.0f;
const float temp = sum[i] * sum[i];
var[i] = var[i] > temp ? sqrt(var[i] - temp) : 0.0f;
}
/* Choose the region with lowest variance. */
float min_var = FLT_MAX;
int min_index = 0;
for (int i = 0; i < 4; i++) {
if (var[i] < min_var) {
min_var = var[i];
min_index = i;
}
}
output[ch] = mean[min_index];
}
}
void KuwaharaClassicOperation::set_kernel_size(int kernel_size)
{
kernel_size_ = kernel_size;
}
int KuwaharaClassicOperation::get_kernel_size()
{
return kernel_size_;
}
void KuwaharaClassicOperation::update_memory_buffer_partial(MemoryBuffer *output,
const rcti &area,
Span<MemoryBuffer *> inputs)
{
MemoryBuffer *image = inputs[0];
for (BuffersIterator<float> it = output->iterate_with(inputs, area); !it.is_end(); ++it) {
const int x = it.x;
const int y = it.y;
it.out[3] = image->get_value(x, y, 3);
for (int ch = 0; ch < 3; ch++) {
float sum[4] = {0.0f, 0.0f, 0.0f, 0.0f};
float mean[4] = {0.0f, 0.0f, 0.0f, 0.0f};
float var[4] = {0.0f, 0.0f, 0.0f, 0.0f};
int cnt[4] = {0, 0, 0, 0};
/* 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 < image->get_width() && yy < image->get_height()) {
const float v = image->get_value(xx, yy, ch);
float color[4];
image->read_elem(xx, yy, color);
const float lum = IMB_colormanagement_get_luminance(color);
if (dx <= 0 && dy <= 0) {
mean[0] += v;
sum[0] += lum;
var[0] += lum * lum;
cnt[0]++;
}
if (dx >= 0 && dy <= 0) {
mean[1] += v;
sum[1] += lum;
var[1] += lum * lum;
cnt[1]++;
}
if (dx <= 0 && dy >= 0) {
mean[2] += v;
sum[2] += lum;
var[2] += lum * lum;
cnt[2]++;
}
if (dx >= 0 && dy >= 0) {
mean[3] += v;
sum[3] += lum;
var[3] += lum * lum;
cnt[3]++;
}
}
}
}
/* Compute region variances. */
for (int i = 0; i < 4; i++) {
mean[i] = cnt[i] != 0 ? mean[i] / cnt[i] : 0.0f;
sum[i] = cnt[i] != 0 ? sum[i] / cnt[i] : 0.0f;
var[i] = cnt[i] != 0 ? var[i] / cnt[i] : 0.0f;
const float temp = sum[i] * sum[i];
var[i] = var[i] > temp ? sqrt(var[i] - temp) : 0.0f;
}
/* Choose the region with lowest variance. */
float min_var = FLT_MAX;
int min_index = 0;
for (int i = 0; i < 4; i++) {
if (var[i] < min_var) {
min_var = var[i];
min_index = i;
}
}
output->get_value(x, y, ch) = mean[min_index];
}
}
}
} // namespace blender::compositor

View File

@ -0,0 +1,31 @@
/* SPDX-FileCopyrightText: 2023 Blender Foundation
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "COM_MultiThreadedOperation.h"
namespace blender::compositor {
class KuwaharaClassicOperation : public MultiThreadedOperation {
SocketReader *image_reader_;
int kernel_size_;
public:
KuwaharaClassicOperation();
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

@ -881,6 +881,12 @@ typedef struct NodeBilateralBlurData {
char _pad[2];
} NodeBilateralBlurData;
typedef struct NodeKuwaharaData {
short size;
short variation;
int smoothing;
} NodeKuwaharaData;
typedef struct NodeAntiAliasingData {
float threshold;
float contrast_limit;
@ -2136,6 +2142,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;
/* Stabilize 2D node. Stored in custom1. */
typedef enum CMPNodeStabilizeInterpolation {
CMP_NODE_STABILIZE_INTERPOLATION_NEAREST = 0,

View File

@ -9374,6 +9374,43 @@ static void def_cmp_denoise(StructRNA *srna)
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update");
}
static void def_cmp_kuwahara(StructRNA *srna)
{
PropertyRNA *prop;
RNA_def_struct_sdna_from(srna, "NodeKuwaharaData", "storage");
static const EnumPropertyItem variation_items[] = {
{0, "CLASSIC", 0, "Classic", "Fast but less accurate variation"},
{1, "ANISOTROPIC", 0, "Anisotropic", "Accurate but slower variation"},
{0, NULL, 0, NULL, NULL},
};
prop = RNA_def_property(srna, "size", PROP_INT, PROP_NONE);
RNA_def_property_int_sdna(prop, NULL, "size");
RNA_def_property_range(prop, 1.0, 100.0);
RNA_def_property_ui_range(prop, 1, 100, 1, -1);
RNA_def_property_ui_text(
prop, "Size", "Size of filter. Larger values give stronger stylized effect");
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update");
prop = RNA_def_property(srna, "variation", PROP_ENUM, PROP_NONE);
RNA_def_property_enum_sdna(prop, NULL, "variation");
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, "smoothing", PROP_INT, PROP_NONE);
RNA_def_property_int_sdna(prop, NULL, "smoothing");
RNA_def_property_range(prop, 0.0, 50.0);
RNA_def_property_ui_range(prop, 0, 50, 1, -1);
RNA_def_property_ui_text(prop,
"Smoothing",
"Smoothing degree before applying filter. Higher values remove details "
"and give smoother edges");
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update");
}
static void def_cmp_antialiasing(StructRNA *srna)
{
PropertyRNA *prop;

View File

@ -224,6 +224,7 @@ DefNode(CompositorNode, CMP_NODE_COMBINE_XYZ, 0, "COMBIN
DefNode(CompositorNode, CMP_NODE_SEPARATE_XYZ, 0, "SEPARATE_XYZ", SeparateXYZ, "Separate XYZ", "" )
DefNode(CompositorNode, CMP_NODE_SEPARATE_COLOR, def_cmp_combsep_color, "SEPARATE_COLOR", SeparateColor, "Separate Color", "" )
DefNode(CompositorNode, CMP_NODE_COMBINE_COLOR, def_cmp_combsep_color, "COMBINE_COLOR", CombineColor, "Combine Color", "" )
DefNode(CompositorNode, CMP_NODE_KUWAHARA, def_cmp_kuwahara, "KUWAHARA", Kuwahara, "Kuwahara", "" )
DefNode(TextureNode, TEX_NODE_OUTPUT, def_tex_output, "OUTPUT", Output, "Output", "" )
DefNode(TextureNode, TEX_NODE_CHECKER, 0, "CHECKER", Checker, "Checker", "" )

View File

@ -77,6 +77,7 @@ set(SRC
nodes/node_composite_invert.cc
nodes/node_composite_keying.cc
nodes/node_composite_keyingscreen.cc
nodes/node_composite_kuwahara.cc
nodes/node_composite_lensdist.cc
nodes/node_composite_levels.cc
nodes/node_composite_luma_matte.cc

View File

@ -64,6 +64,7 @@ void register_composite_nodes()
register_node_type_cmp_invert();
register_node_type_cmp_keying();
register_node_type_cmp_keyingscreen();
register_node_type_cmp_kuwahara();
register_node_type_cmp_lensdist();
register_node_type_cmp_luma_matte();
register_node_type_cmp_map_range();

View File

@ -60,6 +60,7 @@ void register_node_type_cmp_inpaint();
void register_node_type_cmp_invert();
void register_node_type_cmp_keying();
void register_node_type_cmp_keyingscreen();
void register_node_type_cmp_kuwahara();
void register_node_type_cmp_lensdist();
void register_node_type_cmp_luma_matte();
void register_node_type_cmp_map_range();

View File

@ -0,0 +1,87 @@
/* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright 2023 Blender Foundation */
/** \file
* \ingroup cmpnodes
*/
#include "COM_node_operation.hh"
zazizizou marked this conversation as resolved Outdated

Remove the commented out code.

Remove the commented out code.
/* **************** Kuwahara ******************** */
namespace blender::nodes::node_composite_kuwahara_cc {
NODE_STORAGE_FUNCS(NodeKuwaharaData)
static void cmp_node_kuwahara_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Color>(N_("Image"))
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.compositor_domain_priority(0);
b.add_output<decl::Color>(N_("Image"));
}
static void node_composit_init_kuwahara(bNodeTree * /*ntree*/, bNode *node)
{
NodeKuwaharaData *data = MEM_cnew<NodeKuwaharaData>(__func__);
node->storage = data;
/* Set defaults. */
data->size = 4;
data->smoothing = 2;
}
static void node_composit_buts_kuwahara(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr)
{
uiLayout *col;
col = uiLayoutColumn(layout, false);
uiItemR(col, ptr, "variation", 0, nullptr, ICON_NONE);
uiItemR(col, ptr, "size", 0, nullptr, ICON_NONE);
const int variation = RNA_enum_get(ptr, "variation");
if (variation == CMP_NODE_KUWAHARA_ANISOTROPIC) {
uiItemR(col, ptr, "smoothing", 0, nullptr, ICON_NONE);
}
}
using namespace blender::realtime_compositor;
class ConvertKuwaharaOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
get_input("Image").pass_through(get_result("Image"));
context().set_info_message("Viewport compositor setup not fully supported");
}
};
static NodeOperation *get_compositor_operation(Context &context, DNode node)
{
return new ConvertKuwaharaOperation(context, node);
}
} // namespace blender::nodes::node_composite_kuwahara_cc
void register_node_type_cmp_kuwahara()
{
namespace file_ns = blender::nodes::node_composite_kuwahara_cc;
static bNodeType ntype;
cmp_node_type_base(&ntype, CMP_NODE_KUWAHARA, "Kuwahara", NODE_CLASS_OP_FILTER);
ntype.declare = file_ns::cmp_node_kuwahara_declare;
ntype.draw_buttons = file_ns::node_composit_buts_kuwahara;
ntype.initfunc = file_ns::node_composit_init_kuwahara;
node_type_storage(
&ntype, "NodeKuwaharaData", node_free_standard_storage, node_copy_standard_storage);
ntype.get_compositor_operation = file_ns::get_compositor_operation;
ntype.realtime_compositor_unsupported_message = N_(
"Node not supported in the Viewport compositor");
nodeRegisterType(&ntype);
}