In particular, "Inverse Square" is only extracted in the Curve translation context. Reported by Ye Gui in #43295.
844 lines
29 KiB
C++
844 lines
29 KiB
C++
/* SPDX-FileCopyrightText: 2011 Blender Authors
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
|
|
/** \file
|
|
* \ingroup cmpnodes
|
|
*/
|
|
|
|
#include "BLI_math_color.h"
|
|
#include "BLI_math_vector_types.hh"
|
|
|
|
#include "DNA_scene_types.h"
|
|
|
|
#include "RNA_enum_types.hh"
|
|
|
|
#include "GPU_shader.hh"
|
|
|
|
#include "COM_algorithm_morphological_distance.hh"
|
|
#include "COM_algorithm_morphological_distance_feather.hh"
|
|
#include "COM_algorithm_symmetric_separable_blur.hh"
|
|
#include "COM_node_operation.hh"
|
|
#include "COM_utilities.hh"
|
|
|
|
#include "node_composite_util.hh"
|
|
|
|
namespace blender::nodes::node_composite_keying_cc {
|
|
|
|
static void cmp_node_keying_declare(NodeDeclarationBuilder &b)
|
|
{
|
|
b.use_custom_socket_order();
|
|
b.allow_any_socket_order();
|
|
|
|
b.add_input<decl::Color>("Image")
|
|
.default_value({0.8f, 0.8f, 0.8f, 1.0f})
|
|
.hide_value()
|
|
.structure_type(StructureType::Dynamic);
|
|
b.add_output<decl::Color>("Image").structure_type(StructureType::Dynamic).align_with_previous();
|
|
|
|
b.add_output<decl::Float>("Matte").structure_type(StructureType::Dynamic);
|
|
b.add_output<decl::Float>("Edges")
|
|
.structure_type(StructureType::Dynamic)
|
|
.translation_context(BLT_I18NCONTEXT_ID_IMAGE);
|
|
|
|
b.add_input<decl::Color>("Key Color")
|
|
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
|
|
.structure_type(StructureType::Dynamic);
|
|
|
|
PanelDeclarationBuilder &preprocess_panel = b.add_panel("Preprocess").default_closed(true);
|
|
preprocess_panel.add_input<decl::Int>("Blur Size", "Preprocess Blur Size")
|
|
.default_value(0)
|
|
.min(0)
|
|
.description(
|
|
"Blur the color of the input image in YCC color space before keying while leaving the "
|
|
"luminance intact using a Gaussian blur of the given size");
|
|
|
|
PanelDeclarationBuilder &key_panel = b.add_panel("Key").default_closed(true).translation_context(
|
|
BLT_I18NCONTEXT_ID_NODETREE);
|
|
key_panel.add_input<decl::Float>("Balance", "Key Balance")
|
|
.default_value(0.5f)
|
|
.subtype(PROP_FACTOR)
|
|
.min(0.0f)
|
|
.max(1.0f)
|
|
.description(
|
|
"Balances between the two non primary color channels that the primary channel compares "
|
|
"against. 0 means the latter channel of the two is used, while 1 means the former of "
|
|
"the two is used");
|
|
|
|
PanelDeclarationBuilder &tweak_panel = b.add_panel("Tweak").default_closed(true);
|
|
tweak_panel.add_input<decl::Float>("Black Level")
|
|
.default_value(0.0f)
|
|
.subtype(PROP_FACTOR)
|
|
.min(0.0f)
|
|
.max(1.0f)
|
|
.description(
|
|
"The matte gets remapped such matte values lower than the black level become black. "
|
|
"Pixels at the identified edges are excluded from the remapping to preserve details");
|
|
tweak_panel.add_input<decl::Float>("White Level")
|
|
.default_value(1.0f)
|
|
.subtype(PROP_FACTOR)
|
|
.min(0.0f)
|
|
.max(1.0f)
|
|
.description(
|
|
"The matte gets remapped such matte values higher than the white level become white. "
|
|
"Pixels at the identified edges are excluded from the remapping to preserve details");
|
|
|
|
PanelDeclarationBuilder &edges_panel =
|
|
tweak_panel.add_panel("Edges").default_closed(true).translation_context(
|
|
BLT_I18NCONTEXT_ID_IMAGE);
|
|
edges_panel.add_input<decl::Int>("Size", "Edge Search Size")
|
|
.default_value(3)
|
|
.min(0)
|
|
.description(
|
|
"Size of the search window used to identify edges. Higher search size corresponds to "
|
|
"less noisy and higher quality edges, not necessarily bigger edges. Edge tolerance can "
|
|
"be used to expend the size of the edges");
|
|
edges_panel.add_input<decl::Float>("Tolerance", "Edge Tolerance")
|
|
.default_value(0.1f)
|
|
.subtype(PROP_FACTOR)
|
|
.min(0.0f)
|
|
.max(1.0f)
|
|
.description(
|
|
"Pixels are considered part of the edges if more than 10% of the neighbouring pixels "
|
|
"have matte values that differ from the pixel's matte value by this tolerance");
|
|
|
|
PanelDeclarationBuilder &mask_panel = b.add_panel("Mask").default_closed(true);
|
|
mask_panel.add_input<decl::Float>("Garbage Matte")
|
|
.default_value(0.0f)
|
|
.subtype(PROP_FACTOR)
|
|
.min(0.0f)
|
|
.max(1.0f)
|
|
.structure_type(StructureType::Dynamic)
|
|
.description("Areas in the garbage matte mask are excluded from the matte");
|
|
mask_panel.add_input<decl::Float>("Core Matte")
|
|
.default_value(0.0f)
|
|
.subtype(PROP_FACTOR)
|
|
.min(0.0f)
|
|
.max(1.0f)
|
|
.structure_type(StructureType::Dynamic)
|
|
.description("Areas in the core matte mask are included in the matte");
|
|
|
|
PanelDeclarationBuilder &postprocess_panel = b.add_panel("Postprocess").default_closed(true);
|
|
postprocess_panel.add_input<decl::Int>("Blur Size", "Postprocess Blur Size")
|
|
.default_value(0)
|
|
.min(0)
|
|
.description("Blur the computed matte using a Gaussian blur of the given size");
|
|
postprocess_panel.add_input<decl::Int>("Dilate Size", "Postprocess Dilate Size")
|
|
.default_value(0)
|
|
.description(
|
|
"Dilate or erode the computed matte using a circular structuring element of the "
|
|
"specified size. Negative sizes means erosion while positive means dilation");
|
|
postprocess_panel.add_input<decl::Int>("Feather Size", "Postprocess Feather Size")
|
|
.default_value(0)
|
|
.description(
|
|
"Dilate or erode the computed matte using an inverse distance operation evaluated at "
|
|
"the given falloff of the specified size. Negative sizes means erosion while positive "
|
|
"means dilation");
|
|
postprocess_panel.add_input<decl::Menu>("Feather Falloff")
|
|
.default_value(PROP_SMOOTH)
|
|
.static_items(rna_enum_proportional_falloff_curve_only_items)
|
|
.optional_label()
|
|
.translation_context(BLT_I18NCONTEXT_ID_CURVE_LEGACY);
|
|
|
|
PanelDeclarationBuilder &despill_panel = b.add_panel("Despill").default_closed(true);
|
|
despill_panel.add_input<decl::Float>("Strength", "Despill Strength")
|
|
.default_value(1.0f)
|
|
.subtype(PROP_FACTOR)
|
|
.min(0.0f)
|
|
.max(1.0f)
|
|
.description("Specifies the strength of the despill");
|
|
despill_panel.add_input<decl::Float>("Balance", "Despill Balance")
|
|
.default_value(0.5f)
|
|
.subtype(PROP_FACTOR)
|
|
.min(0.0f)
|
|
.max(1.0f)
|
|
.description(
|
|
"Defines the channel used for despill limiting. Balances between the two non primary "
|
|
"color channels that the primary channel compares against. 0 means the latter channel "
|
|
"of the two is used, while 1 means the former of the two is used");
|
|
}
|
|
|
|
static void node_composit_init_keying(bNodeTree * /*ntree*/, bNode *node)
|
|
{
|
|
/* Unused, only kept for forward compatibility. */
|
|
NodeKeyingData *data = MEM_callocN<NodeKeyingData>(__func__);
|
|
node->storage = data;
|
|
}
|
|
|
|
using namespace blender::compositor;
|
|
|
|
class KeyingOperation : public NodeOperation {
|
|
public:
|
|
using NodeOperation::NodeOperation;
|
|
|
|
void execute() override
|
|
{
|
|
const Result &input_image = get_input("Image");
|
|
Result &output_image = get_result("Image");
|
|
Result &output_matte = get_result("Matte");
|
|
Result &output_edges = get_result("Edges");
|
|
if (input_image.is_single_value()) {
|
|
if (output_image.should_compute()) {
|
|
output_image.share_data(input_image);
|
|
}
|
|
if (output_matte.should_compute()) {
|
|
output_matte.allocate_invalid();
|
|
}
|
|
if (output_edges.should_compute()) {
|
|
output_edges.allocate_invalid();
|
|
}
|
|
return;
|
|
}
|
|
|
|
Result blurred_input = compute_blurred_input();
|
|
|
|
Result matte = compute_matte(blurred_input);
|
|
blurred_input.release();
|
|
|
|
/* This also computes the edges output if needed. */
|
|
Result tweaked_matte = compute_tweaked_matte(matte);
|
|
matte.release();
|
|
|
|
if (output_image.should_compute() || output_matte.should_compute()) {
|
|
Result blurred_matte = compute_blurred_matte(tweaked_matte);
|
|
tweaked_matte.release();
|
|
|
|
Result morphed_matte = compute_morphed_matte(blurred_matte);
|
|
blurred_matte.release();
|
|
|
|
Result feathered_matte = compute_feathered_matte(morphed_matte);
|
|
morphed_matte.release();
|
|
|
|
if (output_image.should_compute()) {
|
|
compute_image(feathered_matte);
|
|
}
|
|
|
|
if (output_matte.should_compute()) {
|
|
output_matte.steal_data(feathered_matte);
|
|
}
|
|
else {
|
|
feathered_matte.release();
|
|
}
|
|
}
|
|
else {
|
|
tweaked_matte.release();
|
|
}
|
|
}
|
|
|
|
Result compute_blurred_input()
|
|
{
|
|
/* No blur needed, return the original matte. We also increment the reference count of the
|
|
* input because the caller will release it after the call, and we want to extend its life
|
|
* since it is now returned as the output. */
|
|
const float blur_size = this->get_preprocess_blur_size();
|
|
if (blur_size == 0.0f) {
|
|
Result output = get_input("Image");
|
|
output.increment_reference_count();
|
|
return output;
|
|
}
|
|
|
|
Result chroma = extract_input_chroma();
|
|
|
|
Result blurred_chroma = context().create_result(ResultType::Color);
|
|
symmetric_separable_blur(
|
|
context(), chroma, blurred_chroma, float2(blur_size) / 2, R_FILTER_BOX);
|
|
chroma.release();
|
|
|
|
Result blurred_input = replace_input_chroma(blurred_chroma);
|
|
blurred_chroma.release();
|
|
|
|
return blurred_input;
|
|
}
|
|
|
|
int get_preprocess_blur_size()
|
|
{
|
|
return math::max(0, this->get_input("Preprocess Blur Size").get_single_value_default(0));
|
|
}
|
|
|
|
Result extract_input_chroma()
|
|
{
|
|
if (this->context().use_gpu()) {
|
|
return this->extract_input_chroma_gpu();
|
|
}
|
|
return this->extract_input_chroma_cpu();
|
|
}
|
|
|
|
Result extract_input_chroma_gpu()
|
|
{
|
|
gpu::Shader *shader = context().get_shader("compositor_keying_extract_chroma");
|
|
GPU_shader_bind(shader);
|
|
|
|
Result &input = get_input("Image");
|
|
input.bind_as_texture(shader, "input_tx");
|
|
|
|
Result output = context().create_result(ResultType::Color);
|
|
output.allocate_texture(input.domain());
|
|
output.bind_as_image(shader, "output_img");
|
|
|
|
compute_dispatch_threads_at_least(shader, input.domain().size);
|
|
|
|
GPU_shader_unbind();
|
|
input.unbind_as_texture();
|
|
output.unbind_as_image();
|
|
|
|
return output;
|
|
}
|
|
|
|
Result extract_input_chroma_cpu()
|
|
{
|
|
Result &input = get_input("Image");
|
|
|
|
Result output = context().create_result(ResultType::Color);
|
|
output.allocate_texture(input.domain());
|
|
|
|
parallel_for(input.domain().size, [&](const int2 texel) {
|
|
const float4 color = input.load_pixel<float4>(texel);
|
|
float4 color_ycca;
|
|
rgb_to_ycc(color.x,
|
|
color.y,
|
|
color.z,
|
|
&color_ycca.x,
|
|
&color_ycca.y,
|
|
&color_ycca.z,
|
|
BLI_YCC_ITU_BT709);
|
|
color_ycca /= 255.0f;
|
|
color_ycca.w = color.w;
|
|
|
|
output.store_pixel(texel, color_ycca);
|
|
});
|
|
|
|
return output;
|
|
}
|
|
|
|
Result replace_input_chroma(Result &new_chroma)
|
|
{
|
|
if (this->context().use_gpu()) {
|
|
return this->replace_input_chroma_gpu(new_chroma);
|
|
}
|
|
return this->replace_input_chroma_cpu(new_chroma);
|
|
}
|
|
|
|
Result replace_input_chroma_gpu(Result &new_chroma)
|
|
{
|
|
gpu::Shader *shader = context().get_shader("compositor_keying_replace_chroma");
|
|
GPU_shader_bind(shader);
|
|
|
|
Result &input = get_input("Image");
|
|
input.bind_as_texture(shader, "input_tx");
|
|
|
|
new_chroma.bind_as_texture(shader, "new_chroma_tx");
|
|
|
|
Result output = context().create_result(ResultType::Color);
|
|
output.allocate_texture(input.domain());
|
|
output.bind_as_image(shader, "output_img");
|
|
|
|
compute_dispatch_threads_at_least(shader, input.domain().size);
|
|
|
|
GPU_shader_unbind();
|
|
input.unbind_as_texture();
|
|
new_chroma.unbind_as_texture();
|
|
output.unbind_as_image();
|
|
|
|
return output;
|
|
}
|
|
|
|
Result replace_input_chroma_cpu(Result &new_chroma)
|
|
{
|
|
Result &input = get_input("Image");
|
|
|
|
Result output = context().create_result(ResultType::Color);
|
|
output.allocate_texture(input.domain());
|
|
|
|
parallel_for(input.domain().size, [&](const int2 texel) {
|
|
const float4 color = input.load_pixel<float4>(texel);
|
|
float4 color_ycca;
|
|
rgb_to_ycc(color.x,
|
|
color.y,
|
|
color.z,
|
|
&color_ycca.x,
|
|
&color_ycca.y,
|
|
&color_ycca.z,
|
|
BLI_YCC_ITU_BT709);
|
|
|
|
const float2 new_chroma_cb_cr = new_chroma.load_pixel<float4>(texel).yz();
|
|
color_ycca.y = new_chroma_cb_cr.x * 255.0f;
|
|
color_ycca.z = new_chroma_cb_cr.y * 255.0f;
|
|
|
|
float4 color_rgba;
|
|
ycc_to_rgb(color_ycca.x,
|
|
color_ycca.y,
|
|
color_ycca.z,
|
|
&color_rgba.x,
|
|
&color_rgba.y,
|
|
&color_rgba.z,
|
|
BLI_YCC_ITU_BT709);
|
|
color_rgba.w = color.w;
|
|
|
|
output.store_pixel(texel, color_rgba);
|
|
});
|
|
|
|
return output;
|
|
}
|
|
|
|
Result compute_matte(Result &input)
|
|
{
|
|
if (this->context().use_gpu()) {
|
|
return this->compute_matte_gpu(input);
|
|
}
|
|
return this->compute_matte_cpu(input);
|
|
}
|
|
|
|
Result compute_matte_gpu(Result &input)
|
|
{
|
|
gpu::Shader *shader = context().get_shader("compositor_keying_compute_matte");
|
|
GPU_shader_bind(shader);
|
|
|
|
GPU_shader_uniform_1f(shader, "key_balance", this->get_key_balance());
|
|
|
|
input.bind_as_texture(shader, "input_tx");
|
|
|
|
Result &key_color = get_input("Key Color");
|
|
key_color.bind_as_texture(shader, "key_tx");
|
|
|
|
Result output = context().create_result(ResultType::Float);
|
|
output.allocate_texture(input.domain());
|
|
output.bind_as_image(shader, "output_img");
|
|
|
|
compute_dispatch_threads_at_least(shader, input.domain().size);
|
|
|
|
GPU_shader_unbind();
|
|
input.unbind_as_texture();
|
|
key_color.unbind_as_texture();
|
|
output.unbind_as_image();
|
|
|
|
return output;
|
|
}
|
|
|
|
Result compute_matte_cpu(Result &input)
|
|
{
|
|
const float key_balance = this->get_key_balance();
|
|
|
|
Result &key = get_input("Key Color");
|
|
|
|
Result output = context().create_result(ResultType::Float);
|
|
output.allocate_texture(input.domain());
|
|
|
|
auto compute_saturation_indices = [](const float3 &v) {
|
|
int index_of_max = ((v.x > v.y) ? ((v.x > v.z) ? 0 : 2) : ((v.y > v.z) ? 1 : 2));
|
|
int2 other_indices = (int2(index_of_max) + int2(1, 2)) % 3;
|
|
int min_index = math::min(other_indices.x, other_indices.y);
|
|
int max_index = math::max(other_indices.x, other_indices.y);
|
|
return int3(index_of_max, max_index, min_index);
|
|
};
|
|
|
|
auto compute_saturation = [&](const float4 &color, const int3 &indices) {
|
|
float weighted_average = math::interpolate(color[indices.y], color[indices.z], key_balance);
|
|
return (color[indices.x] - weighted_average) * math::abs(1.0f - weighted_average);
|
|
};
|
|
|
|
parallel_for(input.domain().size, [&](const int2 texel) {
|
|
float4 input_color = input.load_pixel<float4>(texel);
|
|
|
|
/* We assume that the keying screen will not be overexposed in the image, so if the input
|
|
* brightness is high, we assume the pixel is opaque. */
|
|
if (math::reduce_min(input_color) > 1.0f) {
|
|
output.store_pixel(texel, 1.0f);
|
|
return;
|
|
}
|
|
|
|
float4 key_color = key.load_pixel<float4, true>(texel);
|
|
int3 key_saturation_indices = compute_saturation_indices(key_color.xyz());
|
|
float input_saturation = compute_saturation(input_color, key_saturation_indices);
|
|
float key_saturation = compute_saturation(key_color, key_saturation_indices);
|
|
|
|
float matte;
|
|
if (input_saturation < 0.0f) {
|
|
/* Means main channel of pixel is different from screen, assume this is completely a
|
|
* foreground. */
|
|
matte = 1.0f;
|
|
}
|
|
else if (input_saturation >= key_saturation) {
|
|
/* Matched main channels and higher saturation on pixel is treated as completely
|
|
* background. */
|
|
matte = 0.0f;
|
|
}
|
|
else {
|
|
matte = 1.0f - math::clamp(input_saturation / key_saturation, 0.0f, 1.0f);
|
|
}
|
|
|
|
output.store_pixel(texel, matte);
|
|
});
|
|
|
|
return output;
|
|
}
|
|
|
|
float get_key_balance()
|
|
{
|
|
return math::clamp(this->get_input("Key Balance").get_single_value_default(0.5f), 0.0f, 1.0f);
|
|
}
|
|
|
|
Result compute_tweaked_matte(Result &input_matte)
|
|
{
|
|
const float black_level = this->get_black_level();
|
|
const float white_level = this->get_black_level();
|
|
const Result &garbage_matte = this->get_input("Garbage Matte");
|
|
const Result &core_matte = this->get_input("Core Matte");
|
|
|
|
/* The edges output is not needed and the matte is not tweaked, so return the original matte.
|
|
* We also increment the reference count of the input because the caller will release it after
|
|
* the call, and we want to extend its life since it is now returned as the output. */
|
|
Result &output_edges = get_result("Edges");
|
|
if (!output_edges.should_compute() && black_level == 0.0f && white_level == 1.0f &&
|
|
core_matte.get_single_value_default(1.0f) == 0.0f &&
|
|
garbage_matte.get_single_value_default(1.0f) == 0.0f)
|
|
{
|
|
Result output_matte = input_matte;
|
|
input_matte.increment_reference_count();
|
|
return output_matte;
|
|
}
|
|
|
|
if (this->context().use_gpu()) {
|
|
return this->compute_tweaked_matte_gpu(input_matte);
|
|
}
|
|
return this->compute_tweaked_matte_cpu(input_matte);
|
|
}
|
|
|
|
Result compute_tweaked_matte_gpu(Result &input_matte)
|
|
{
|
|
gpu::Shader *shader = context().get_shader(this->get_tweak_matte_shader_name());
|
|
GPU_shader_bind(shader);
|
|
|
|
GPU_shader_uniform_1i(shader, "edge_search_radius", this->get_edge_search_size());
|
|
GPU_shader_uniform_1f(shader, "edge_tolerance", this->get_edge_tolerance());
|
|
GPU_shader_uniform_1f(shader, "black_level", this->get_black_level());
|
|
GPU_shader_uniform_1f(shader, "white_level", this->get_white_level());
|
|
|
|
input_matte.bind_as_texture(shader, "input_matte_tx");
|
|
|
|
Result &garbage_matte = get_input("Garbage Matte");
|
|
garbage_matte.bind_as_texture(shader, "garbage_matte_tx");
|
|
|
|
Result &core_matte = get_input("Core Matte");
|
|
core_matte.bind_as_texture(shader, "core_matte_tx");
|
|
|
|
Result output_matte = context().create_result(ResultType::Float);
|
|
output_matte.allocate_texture(input_matte.domain());
|
|
output_matte.bind_as_image(shader, "output_matte_img");
|
|
|
|
Result &output_edges = get_result("Edges");
|
|
if (output_edges.should_compute()) {
|
|
output_edges.allocate_texture(input_matte.domain());
|
|
output_edges.bind_as_image(shader, "output_edges_img");
|
|
}
|
|
|
|
compute_dispatch_threads_at_least(shader, input_matte.domain().size);
|
|
|
|
GPU_shader_unbind();
|
|
input_matte.unbind_as_texture();
|
|
garbage_matte.unbind_as_texture();
|
|
core_matte.unbind_as_texture();
|
|
output_matte.unbind_as_image();
|
|
if (output_edges.should_compute()) {
|
|
output_edges.unbind_as_image();
|
|
}
|
|
|
|
return output_matte;
|
|
}
|
|
|
|
const char *get_tweak_matte_shader_name()
|
|
{
|
|
Result &output_edges = get_result("Edges");
|
|
return output_edges.should_compute() ? "compositor_keying_tweak_matte_with_edges" :
|
|
"compositor_keying_tweak_matte_without_edges";
|
|
}
|
|
|
|
Result compute_tweaked_matte_cpu(Result &input_matte)
|
|
{
|
|
const int edge_search_radius = this->get_edge_search_size();
|
|
const float edge_tolerance = this->get_edge_tolerance();
|
|
const float black_level = this->get_black_level();
|
|
const float white_level = this->get_white_level();
|
|
|
|
Result &garbage_matte_image = get_input("Garbage Matte");
|
|
Result &core_matte_image = get_input("Core Matte");
|
|
|
|
Result output_matte = context().create_result(ResultType::Float);
|
|
output_matte.allocate_texture(input_matte.domain());
|
|
|
|
Result &output_edges = this->get_result("Edges");
|
|
const bool compute_edges = output_edges.should_compute();
|
|
if (compute_edges) {
|
|
output_edges.allocate_texture(input_matte.domain());
|
|
}
|
|
|
|
parallel_for(input_matte.domain().size, [&](const int2 texel) {
|
|
float matte = input_matte.load_pixel<float>(texel);
|
|
|
|
/* Search the neighborhood around the current matte value and identify if it lies along the
|
|
* edges of the matte. This is needs to be computed only when we need to compute the edges
|
|
* output or tweak the levels of the matte. */
|
|
bool is_edge = false;
|
|
if (compute_edges || black_level != 0.0f || white_level != 1.0f) {
|
|
/* Count the number of neighbors whose matte is sufficiently similar to the current matte,
|
|
* as controlled by the edge_tolerance factor. */
|
|
int count = 0;
|
|
for (int j = -edge_search_radius; j <= edge_search_radius; j++) {
|
|
for (int i = -edge_search_radius; i <= edge_search_radius; i++) {
|
|
float neighbor_matte = input_matte.load_pixel_extended<float>(texel + int2(i, j));
|
|
count += int(math::distance(matte, neighbor_matte) < edge_tolerance);
|
|
}
|
|
}
|
|
|
|
/* If the number of neighbors that are sufficiently similar to the center matte is less
|
|
* that 90% of the total number of neighbors, then that means the variance is high in that
|
|
* areas and it is considered an edge. */
|
|
is_edge = count < ((edge_search_radius * 2 + 1) * (edge_search_radius * 2 + 1)) * 0.9f;
|
|
}
|
|
|
|
float tweaked_matte = matte;
|
|
|
|
/* Remap the matte using the black and white levels, but only for areas that are not on the
|
|
* edge of the matte to preserve details. Also check for equality between levels to avoid
|
|
* zero division. */
|
|
if (!is_edge && white_level != black_level) {
|
|
tweaked_matte = math::clamp(
|
|
(matte - black_level) / (white_level - black_level), 0.0f, 1.0f);
|
|
}
|
|
|
|
/* Exclude unwanted areas using the provided garbage matte, 1 means unwanted, so invert the
|
|
* garbage matte and take the minimum. */
|
|
float garbage_matte = garbage_matte_image.load_pixel<float, true>(texel);
|
|
tweaked_matte = math::min(tweaked_matte, 1.0f - garbage_matte);
|
|
|
|
/* Include wanted areas that were incorrectly keyed using the provided core matte. */
|
|
float core_matte = core_matte_image.load_pixel<float, true>(texel);
|
|
tweaked_matte = math::max(tweaked_matte, core_matte);
|
|
|
|
output_matte.store_pixel(texel, tweaked_matte);
|
|
if (compute_edges) {
|
|
output_edges.store_pixel(texel, is_edge ? 1.0f : 0.0f);
|
|
}
|
|
});
|
|
|
|
return output_matte;
|
|
}
|
|
|
|
int get_edge_search_size()
|
|
{
|
|
return math::max(0, this->get_input("Edge Search Size").get_single_value_default(3));
|
|
}
|
|
|
|
float get_edge_tolerance()
|
|
{
|
|
return math::clamp(
|
|
this->get_input("Edge Tolerance").get_single_value_default(0.1f), 0.0f, 1.0f);
|
|
}
|
|
|
|
float get_black_level()
|
|
{
|
|
return math::clamp(this->get_input("Black Level").get_single_value_default(0.0f), 0.0f, 1.0f);
|
|
}
|
|
|
|
float get_white_level()
|
|
{
|
|
return math::clamp(this->get_input("White Level").get_single_value_default(1.0f), 0.0f, 1.0f);
|
|
}
|
|
|
|
Result compute_blurred_matte(Result &input_matte)
|
|
{
|
|
const float blur_size = this->get_postprocess_blur_size();
|
|
/* No blur needed, return the original matte. We also increment the reference count of the
|
|
* input because the caller will release it after the call, and we want to extend its life
|
|
* since it is now returned as the output. */
|
|
if (blur_size == 0.0f) {
|
|
Result output_matte = input_matte;
|
|
input_matte.increment_reference_count();
|
|
return output_matte;
|
|
}
|
|
|
|
Result blurred_matte = context().create_result(ResultType::Float);
|
|
symmetric_separable_blur(
|
|
context(), input_matte, blurred_matte, float2(blur_size) / 2, R_FILTER_BOX);
|
|
|
|
return blurred_matte;
|
|
}
|
|
|
|
int get_postprocess_blur_size()
|
|
{
|
|
return math::max(0, this->get_input("Postprocess Blur Size").get_single_value_default(0));
|
|
}
|
|
|
|
Result compute_morphed_matte(Result &input_matte)
|
|
{
|
|
const int distance = this->get_postprocess_dilate_size();
|
|
/* No morphology needed, return the original matte. We also increment the reference count of
|
|
* the input because the caller will release it after the call, and we want to extend its life
|
|
* since it is now returned as the output. */
|
|
if (distance == 0) {
|
|
Result output_matte = input_matte;
|
|
input_matte.increment_reference_count();
|
|
return output_matte;
|
|
}
|
|
|
|
Result morphed_matte = context().create_result(ResultType::Float);
|
|
morphological_distance(context(), input_matte, morphed_matte, distance);
|
|
|
|
return morphed_matte;
|
|
}
|
|
|
|
int get_postprocess_dilate_size()
|
|
{
|
|
return this->get_input("Postprocess Dilate Size").get_single_value_default(0);
|
|
}
|
|
|
|
Result compute_feathered_matte(Result &input_matte)
|
|
{
|
|
const int distance = this->get_postprocess_feather_size();
|
|
/* No feathering needed, return the original matte. We also increment the reference count of
|
|
* the input because the caller will release it after the call, and we want to extend its life
|
|
* since it is now returned as the output. */
|
|
if (distance == 0) {
|
|
Result output_matte = input_matte;
|
|
input_matte.increment_reference_count();
|
|
return output_matte;
|
|
}
|
|
|
|
Result feathered_matte = context().create_result(ResultType::Float);
|
|
morphological_distance_feather(
|
|
context(), input_matte, feathered_matte, distance, this->get_feather_falloff());
|
|
|
|
return feathered_matte;
|
|
}
|
|
|
|
int get_postprocess_feather_size()
|
|
{
|
|
return this->get_input("Postprocess Feather Size").get_single_value_default(0);
|
|
}
|
|
|
|
int get_feather_falloff()
|
|
{
|
|
const Result &input = this->get_input("Feather Falloff");
|
|
const MenuValue default_menu_value = MenuValue(PROP_SMOOTH);
|
|
const MenuValue menu_value = input.get_single_value_default(default_menu_value);
|
|
return menu_value.value;
|
|
}
|
|
|
|
void compute_image(Result &matte)
|
|
{
|
|
if (this->context().use_gpu()) {
|
|
this->compute_image_gpu(matte);
|
|
}
|
|
else {
|
|
this->compute_image_cpu(matte);
|
|
}
|
|
}
|
|
|
|
void compute_image_gpu(Result &matte)
|
|
{
|
|
gpu::Shader *shader = context().get_shader("compositor_keying_compute_image");
|
|
GPU_shader_bind(shader);
|
|
|
|
GPU_shader_uniform_1f(shader, "despill_factor", this->get_despill_strength());
|
|
GPU_shader_uniform_1f(shader, "despill_balance", this->get_despill_balance());
|
|
|
|
Result &input = get_input("Image");
|
|
input.bind_as_texture(shader, "input_tx");
|
|
|
|
Result &key = get_input("Key Color");
|
|
key.bind_as_texture(shader, "key_tx");
|
|
|
|
matte.bind_as_texture(shader, "matte_tx");
|
|
|
|
Result &output = get_result("Image");
|
|
output.allocate_texture(matte.domain());
|
|
output.bind_as_image(shader, "output_img");
|
|
|
|
compute_dispatch_threads_at_least(shader, input.domain().size);
|
|
|
|
GPU_shader_unbind();
|
|
input.unbind_as_texture();
|
|
key.unbind_as_texture();
|
|
matte.unbind_as_texture();
|
|
output.unbind_as_image();
|
|
}
|
|
|
|
void compute_image_cpu(Result &matte_image)
|
|
{
|
|
const float despill_factor = this->get_despill_strength();
|
|
const float despill_balance = this->get_despill_balance();
|
|
|
|
Result &input = get_input("Image");
|
|
Result &key = get_input("Key Color");
|
|
|
|
Result &output = get_result("Image");
|
|
output.allocate_texture(matte_image.domain());
|
|
|
|
auto compute_saturation_indices = [](const float3 &v) {
|
|
int index_of_max = ((v.x > v.y) ? ((v.x > v.z) ? 0 : 2) : ((v.y > v.z) ? 1 : 2));
|
|
int2 other_indices = (int2(index_of_max) + int2(1, 2)) % 3;
|
|
int min_index = math::min(other_indices.x, other_indices.y);
|
|
int max_index = math::max(other_indices.x, other_indices.y);
|
|
return int3(index_of_max, max_index, min_index);
|
|
};
|
|
|
|
parallel_for(input.domain().size, [&](const int2 texel) {
|
|
float4 key_color = key.load_pixel<float4, true>(texel);
|
|
float4 color = input.load_pixel<float4>(texel);
|
|
float matte = matte_image.load_pixel<float>(texel);
|
|
|
|
/* Alpha multiply the matte to the image. */
|
|
color *= matte;
|
|
|
|
/* Color despill. */
|
|
int3 indices = compute_saturation_indices(key_color.xyz());
|
|
float weighted_average = math::interpolate(
|
|
color[indices.y], color[indices.z], despill_balance);
|
|
color[indices.x] -= math::max(0.0f, (color[indices.x] - weighted_average) * despill_factor);
|
|
|
|
output.store_pixel(texel, color);
|
|
});
|
|
}
|
|
|
|
float get_despill_strength()
|
|
{
|
|
return math::max(0.0f, this->get_input("Despill Strength").get_single_value_default(1.0f));
|
|
}
|
|
|
|
float get_despill_balance()
|
|
{
|
|
return math::clamp(
|
|
this->get_input("Despill Balance").get_single_value_default(0.5f), 0.0f, 1.0f);
|
|
}
|
|
};
|
|
|
|
static NodeOperation *get_compositor_operation(Context &context, DNode node)
|
|
{
|
|
return new KeyingOperation(context, node);
|
|
}
|
|
|
|
} // namespace blender::nodes::node_composite_keying_cc
|
|
|
|
static void register_node_type_cmp_keying()
|
|
{
|
|
namespace file_ns = blender::nodes::node_composite_keying_cc;
|
|
|
|
static blender::bke::bNodeType ntype;
|
|
|
|
cmp_node_type_base(&ntype, "CompositorNodeKeying", CMP_NODE_KEYING);
|
|
ntype.ui_name = "Keying";
|
|
ntype.ui_description =
|
|
"Perform both chroma keying (to remove the backdrop) and despill (to correct color cast "
|
|
"from the backdrop)";
|
|
ntype.enum_name_legacy = "KEYING";
|
|
ntype.nclass = NODE_CLASS_MATTE;
|
|
ntype.declare = file_ns::cmp_node_keying_declare;
|
|
ntype.initfunc = file_ns::node_composit_init_keying;
|
|
blender::bke::node_type_storage(
|
|
ntype, "NodeKeyingData", node_free_standard_storage, node_copy_standard_storage);
|
|
ntype.get_compositor_operation = file_ns::get_compositor_operation;
|
|
blender::bke::node_type_size(ntype, 155, 140, NODE_DEFAULT_MAX_WIDTH);
|
|
|
|
blender::bke::node_register_type(ntype);
|
|
}
|
|
NOD_REGISTER_NODE(register_node_type_cmp_keying)
|