Compositor: Add High Precision option to Kuwahara

For high radii Kuwahara, we use a Summed Area Table (SAT) implementation
to accelerate the classic variant of the algorithm. The problem is that
due to limited floating point precision, the SAT can produce artifacts
in its output.

An attempt to fix this was implemented in #114191, and while that patch
improved precision by 10x, the artifacts still existed, albeit less
noticeable. But since the improved precision also meant a performance
penalty, it was decided that the improvement is not worth it.

Since the artifacts are only noticeable for scenes with very high
values, this patch adds a High Precision option that defaults to false
and can be enabled by the user upon noticing any artifacts. The option
simply uses direction convolution instead of SAT in this case. The
downside, of course, is that it can be orders of magnitude slower.

An alternative to using this option is for the user to clamp the input
or downsample the image. Both methods should be documented in the
documentation.

Fixes: #113578.

Pull Request: https://projects.blender.org/blender/blender/pulls/115763
This commit is contained in:
Omar Emara
2024-01-17 14:30:29 +01:00
committed by Omar Emara
parent 1e85feb542
commit e4a93d7b8c
6 changed files with 36 additions and 8 deletions
@@ -25,6 +25,7 @@ void KuwaharaNode::convert_to_operations(NodeConverter &converter,
switch (data->variation) {
case CMP_NODE_KUWAHARA_CLASSIC: {
KuwaharaClassicOperation *kuwahara_classic = new KuwaharaClassicOperation();
kuwahara_classic->set_data(data);
converter.add_operation(kuwahara_classic);
converter.map_input_socket(get_input_socket(0), kuwahara_classic->get_input_socket(0));
converter.map_input_socket(get_input_socket(1), kuwahara_classic->get_input_socket(1));
@@ -54,8 +54,10 @@ void KuwaharaClassicOperation::execute_pixel_sampled(float output[4],
size_reader_->read_sampled(size, x, y, sampler);
const int kernel_size = int(math::max(0.0f, size[0]));
/* Naive implementation is more accurate for small kernel sizes. */
if (kernel_size >= 4) {
/* For high radii, we accelerate the filter using a summed area table, making the filter
* execute in constant time as opposed to having quadratic complexity. Except if high precision
* is enabled, since summed area tables are less precise. */
if (!data_->high_precision && size[0] > 5.0f) {
for (int q = 0; q < 4; q++) {
/* A fancy expression to compute the sign of the quadrant q. */
int2 sign = int2((q % 2) * 2 - 1, ((q / 2) * 2 - 1));
@@ -172,10 +174,13 @@ void KuwaharaClassicOperation::update_memory_buffer_partial(MemoryBuffer *output
float4 mean_of_squared_color[4] = {float4(0.0f), float4(0.0f), float4(0.0f), float4(0.0f)};
int quadrant_pixel_count[4] = {0, 0, 0, 0};
const int kernel_size = int(math::max(0.0f, *size_image->get_elem(x, y)));
const float size = *size_image->get_elem(x, y);
const int kernel_size = int(math::max(0.0f, size));
/* Naive implementation is more accurate for small kernel sizes. */
if (kernel_size >= 4) {
/* For high radii, we accelerate the filter using a summed area table, making the filter
* execute in constant time as opposed to having quadratic complexity. Except if high precision
* is enabled, since summed area tables are less precise. */
if (!data_->high_precision && size > 5.0f) {
for (int q = 0; q < 4; q++) {
/* A fancy expression to compute the sign of the quadrant q. */
int2 sign = int2((q % 2) * 2 - 1, ((q / 2) * 2 - 1));
@@ -9,6 +9,7 @@
namespace blender::compositor {
class KuwaharaClassicOperation : public MultiThreadedOperation {
const NodeKuwaharaData *data_;
SocketReader *image_reader_;
SocketReader *size_reader_;
SocketReader *sat_reader_;
@@ -17,6 +18,11 @@ class KuwaharaClassicOperation : public MultiThreadedOperation {
public:
KuwaharaClassicOperation();
void set_data(const NodeKuwaharaData *data)
{
data_ = data;
}
void init_execution() override;
void deinit_execution() override;
void execute_pixel_sampled(float output[4], float x, float y, PixelSampler sampler) override;
+2
View File
@@ -1067,6 +1067,8 @@ typedef struct NodeKuwaharaData {
int uniformity;
float sharpness;
float eccentricity;
char high_precision;
char _pad[3];
} NodeKuwaharaData;
typedef struct NodeAntiAliasingData {
@@ -8582,6 +8582,14 @@ static void def_cmp_kuwahara(StructRNA *srna)
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, "use_high_precision", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, nullptr, "high_precision", 1);
RNA_def_property_ui_text(
prop,
"High Precision",
"Uses a more precise but slower method. Use if the output contains undesirable noise");
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update");
prop = RNA_def_property(srna, "uniformity", PROP_INT, PROP_NONE);
RNA_def_property_int_sdna(prop, nullptr, "uniformity");
RNA_def_property_range(prop, 0.0, 50.0);
@@ -57,7 +57,10 @@ static void node_composit_buts_kuwahara(uiLayout *layout, bContext * /*C*/, Poin
const int variation = RNA_enum_get(ptr, "variation");
if (variation == CMP_NODE_KUWAHARA_ANISOTROPIC) {
if (variation == CMP_NODE_KUWAHARA_CLASSIC) {
uiItemR(col, ptr, "use_high_precision", UI_ITEM_NONE, nullptr, ICON_NONE);
}
else if (variation == CMP_NODE_KUWAHARA_ANISOTROPIC) {
uiItemR(col, ptr, "uniformity", UI_ITEM_NONE, nullptr, ICON_NONE);
uiItemR(col, ptr, "sharpness", UI_ITEM_NONE, nullptr, ICON_NONE);
uiItemR(col, ptr, "eccentricity", UI_ITEM_NONE, nullptr, ICON_NONE);
@@ -88,9 +91,12 @@ class ConvertKuwaharaOperation : public NodeOperation {
void execute_classic()
{
/* For high radii, we accelerate the filter using a summed area table, making the filter
* execute in constant time as opposed to the trivial quadratic complexity. */
* execute in constant time as opposed to having quadratic complexity. Except if high precision
* is enabled, since summed area tables are less precise. */
Result &size_input = get_input("Size");
if (size_input.is_single_value() && size_input.get_float_value() > 5.0f) {
if (!node_storage(bnode()).high_precision &&
(size_input.is_texture() || size_input.get_float_value() > 5.0f))
{
execute_classic_summed_area_table();
return;
}