Blender V4.3
COM_KuwaharaAnisotropicOperation.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2023 Blender Authors
2 *
3 * SPDX-License-Identifier: GPL-2.0-or-later */
4
6
7#include "BLI_math_base.h"
8#include "BLI_math_base.hh"
10#include "BLI_math_vector.h"
11#include "BLI_math_vector.hh"
13
14namespace blender::compositor {
15
24
25/* An implementation of the Anisotropic Kuwahara filter described in the paper:
26 *
27 * Kyprianidis, Jan Eric, Henry Kang, and Jurgen Dollner. "Image and video abstraction by
28 * anisotropic Kuwahara filtering." 2009.
29 *
30 * But with the polynomial weighting functions described in the paper:
31 *
32 * Kyprianidis, Jan Eric, et al. "Anisotropic Kuwahara Filtering with Polynomial Weighting
33 * Functions." 2010.
34 *
35 * And the sector weight function described in the paper:
36 *
37 * Kyprianidis, Jan Eric. "Image and video abstraction by multi-scale anisotropic Kuwahara
38 * filtering." 2011.
39 */
41 const rcti &area,
43{
44 using namespace math;
45 for (BuffersIterator<float> it = output->iterate_with(inputs, area); !it.is_end(); ++it) {
46 /* The structure tensor is encoded in a float4 using a column major storage order, as can be
47 * seen in the KuwaharaAnisotropicStructureTensorOperation. */
48 float4 encoded_structure_tensor = float4(inputs[2]->get_elem(it.x, it.y));
49 float dxdx = encoded_structure_tensor.x;
50 float dxdy = encoded_structure_tensor.y;
51 float dydy = encoded_structure_tensor.w;
52
53 /* Compute the first and second eigenvalues of the structure tensor using the equations in
54 * section "3.1 Orientation and Anisotropy Estimation" of the paper. */
55 float eigenvalue_first_term = (dxdx + dydy) / 2.0f;
56 float eigenvalue_square_root_term = sqrt(square(dxdx - dydy) + 4.0f * square(dxdy)) / 2.0f;
57 float first_eigenvalue = eigenvalue_first_term + eigenvalue_square_root_term;
58 float second_eigenvalue = eigenvalue_first_term - eigenvalue_square_root_term;
59
60 /* Compute the normalized eigenvector of the structure tensor oriented in direction of the
61 * minimum rate of change using the equations in section "3.1 Orientation and Anisotropy
62 * Estimation" of the paper. */
63 float2 eigenvector = float2(first_eigenvalue - dxdx, -dxdy);
64 float eigenvector_length = length(eigenvector);
65 float2 unit_eigenvector = eigenvector_length != 0.0f ? eigenvector / eigenvector_length :
66 float2(1.0f);
67
68 /* Compute the amount of anisotropy using equations in section "3.1 Orientation and Anisotropy
69 * Estimation" of the paper. The anisotropy ranges from 0 to 1, where 0 corresponds to
70 * isotropic and 1 corresponds to entirely anisotropic regions. */
71 float eigenvalue_sum = first_eigenvalue + second_eigenvalue;
72 float eigenvalue_difference = first_eigenvalue - second_eigenvalue;
73 float anisotropy = eigenvalue_sum > 0.0f ? eigenvalue_difference / eigenvalue_sum : 0.0f;
74
75 float radius = max(0.0f, *inputs[1]->get_elem(it.x, it.y));
76 if (radius == 0) {
77 copy_v4_v4(it.out, inputs[0]->get_elem(it.x, it.y));
78 continue;
79 }
80
81 /* Compute the width and height of an ellipse that is more width-elongated for high anisotropy
82 * and more circular for low anisotropy, controlled using the eccentricity factor. Since the
83 * anisotropy is in the [0, 1] range, the width factor tends to 1 as the eccentricity tends to
84 * infinity and tends to infinity when the eccentricity tends to zero. This is based on the
85 * equations in section "3.2. Anisotropic Kuwahara Filtering" of the paper. */
86 float ellipse_width_factor = (get_eccentricity() + anisotropy) / get_eccentricity();
87 float ellipse_width = ellipse_width_factor * radius;
88 float ellipse_height = radius / ellipse_width_factor;
89
90 /* Compute the cosine and sine of the angle that the eigenvector makes with the x axis. Since
91 * the eigenvector is normalized, its x and y components are the cosine and sine of the angle
92 * it makes with the x axis. */
93 float cosine = unit_eigenvector.x;
94 float sine = unit_eigenvector.y;
95
96 /* Compute an inverse transformation matrix that represents an ellipse of the given width and
97 * height and makes and an angle with the x axis of the given cosine and sine. This is an
98 * inverse matrix, so it transforms the ellipse into a disk of unit radius. */
99 float2x2 inverse_ellipse_matrix = float2x2(
100 float2(cosine / ellipse_width, -sine / ellipse_height),
101 float2(sine / ellipse_width, cosine / ellipse_height));
102
103 /* Compute the bounding box of a zero centered ellipse whose major axis is aligned with the
104 * eigenvector and has the given width and height. This is based on the equations described in:
105 *
106 * https://iquilezles.org/articles/ellipses/
107 *
108 * Notice that we only compute the upper bound, the lower bound is just negative that since the
109 * ellipse is zero centered. Also notice that we take the ceiling of the bounding box, just to
110 * ensure the filter window is at least 1x1. */
111 float2 ellipse_major_axis = ellipse_width * unit_eigenvector;
112 float2 ellipse_minor_axis = ellipse_height * float2(unit_eigenvector.y, unit_eigenvector.x) *
113 float2(-1, 1);
114 int2 ellipse_bounds = int2(
115 ceil(sqrt(square(ellipse_major_axis) + square(ellipse_minor_axis))));
116
117 /* Compute the overlap polynomial parameters for 8-sector ellipse based on the equations in
118 * section "3 Alternative Weighting Functions" of the polynomial weights paper. More on this
119 * later in the code. */
120 int number_of_sectors = 8;
121 float sector_center_overlap_parameter = 2.0f / radius;
122 float sector_envelope_angle = ((3.0f / 2.0f) * M_PI) / number_of_sectors;
123 float cross_sector_overlap_parameter = (sector_center_overlap_parameter +
124 cos(sector_envelope_angle)) /
125 square(sin(sector_envelope_angle));
126
127 /* We need to compute the weighted mean of color and squared color of each of the 8 sectors of
128 * the ellipse, so we declare arrays for accumulating those and initialize them in the next
129 * code section. */
130 float4 weighted_mean_of_squared_color_of_sectors[8];
131 float4 weighted_mean_of_color_of_sectors[8];
132 float sum_of_weights_of_sectors[8];
133
134 /* The center pixel (0, 0) is exempt from the main loop below for reasons that are explained in
135 * the first if statement in the loop, so we need to accumulate its color, squared color, and
136 * weight separately first. Luckily, the zero coordinates of the center pixel zeros out most of
137 * the complex computations below, and it can easily be shown that the weight for the center
138 * pixel in all sectors is simply (1 / number_of_sectors). */
139 float4 center_color = float4(inputs[0]->get_elem(it.x, it.y));
140 float4 center_color_squared = center_color * center_color;
141 float center_weight = 1.0f / number_of_sectors;
142 float4 weighted_center_color = center_color * center_weight;
143 float4 weighted_center_color_squared = center_color_squared * center_weight;
144 for (int i = 0; i < number_of_sectors; i++) {
145 weighted_mean_of_squared_color_of_sectors[i] = weighted_center_color_squared;
146 weighted_mean_of_color_of_sectors[i] = weighted_center_color;
147 sum_of_weights_of_sectors[i] = center_weight;
148 }
149
150 /* Loop over the window of pixels inside the bounding box of the ellipse. However, we utilize
151 * the fact that ellipses are mirror symmetric along the horizontal axis, so we reduce the
152 * window to only the upper two quadrants, and compute each two mirrored pixels at the same
153 * time using the same weight as an optimization. */
154 for (int j = 0; j <= ellipse_bounds.y; j++) {
155 for (int i = -ellipse_bounds.x; i <= ellipse_bounds.x; i++) {
156 /* Since we compute each two mirrored pixels at the same time, we need to also exempt the
157 * pixels whose x coordinates are negative and their y coordinates are zero, that's because
158 * those are mirrored versions of the pixels whose x coordinates are positive and their y
159 * coordinates are zero, and we don't want to compute and accumulate them twice. Moreover,
160 * we also need to exempt the center pixel with zero coordinates for the same reason,
161 * however, since the mirror of the center pixel is itself, it need to be accumulated
162 * separately, hence why we did that in the code section just before this loop. */
163 if (j == 0 && i <= 0) {
164 continue;
165 }
166
167 /* Map the pixels of the ellipse into a unit disk, exempting any points that are not part
168 * of the ellipse or disk. */
169 float2 disk_point = inverse_ellipse_matrix * float2(i, j);
170 float disk_point_length_squared = dot(disk_point, disk_point);
171 if (disk_point_length_squared > 1.0f) {
172 continue;
173 }
174
175 /* While each pixel belongs to a single sector in the ellipse, we expand the definition of
176 * a sector a bit to also overlap with other sectors as illustrated in Figure 8 of the
177 * polynomial weights paper. So each pixel may contribute to multiple sectors, and thus we
178 * compute its weight in each of the 8 sectors. */
179 float sector_weights[8];
180
181 /* We evaluate the weighting polynomial at each of the 8 sectors by rotating the disk point
182 * by 45 degrees and evaluating the weighting polynomial at each incremental rotation. To
183 * avoid potentially expensive rotations, we utilize the fact that rotations by 90 degrees
184 * are simply swapping of the coordinates and negating the x component. We also note that
185 * since the y term of the weighting polynomial is squared, it is not affected by the sign
186 * and can be computed once for the x and once for the y coordinates. So we compute every
187 * other even-indexed 4 weights by successive 90 degree rotations as discussed. */
188 float2 polynomial = sector_center_overlap_parameter -
189 cross_sector_overlap_parameter * square(disk_point);
190 sector_weights[0] = square(max(0.0f, disk_point.y + polynomial.x));
191 sector_weights[2] = square(max(0.0f, -disk_point.x + polynomial.y));
192 sector_weights[4] = square(max(0.0f, -disk_point.y + polynomial.x));
193 sector_weights[6] = square(max(0.0f, disk_point.x + polynomial.y));
194
195 /* Then we rotate the disk point by 45 degrees, which is a simple expression involving a
196 * constant as can be demonstrated by applying a 45 degree rotation matrix. */
197 float2 rotated_disk_point = M_SQRT1_2 * float2(disk_point.x - disk_point.y,
198 disk_point.x + disk_point.y);
199
200 /* Finally, we compute every other odd-index 4 weights starting from the 45 degree rotated
201 * disk point. */
202 float2 rotated_polynomial = sector_center_overlap_parameter -
203 cross_sector_overlap_parameter * square(rotated_disk_point);
204 sector_weights[1] = square(max(0.0f, rotated_disk_point.y + rotated_polynomial.x));
205 sector_weights[3] = square(max(0.0f, -rotated_disk_point.x + rotated_polynomial.y));
206 sector_weights[5] = square(max(0.0f, -rotated_disk_point.y + rotated_polynomial.x));
207 sector_weights[7] = square(max(0.0f, rotated_disk_point.x + rotated_polynomial.y));
208
209 /* We compute a radial Gaussian weighting component such that pixels further away from the
210 * sector center gets attenuated, and we also divide by the sum of sector weights to
211 * normalize them, since the radial weight will eventually be multiplied to the sector
212 * weight below. */
213 float sector_weights_sum = sector_weights[0] + sector_weights[1] + sector_weights[2] +
214 sector_weights[3] + sector_weights[4] + sector_weights[5] +
215 sector_weights[6] + sector_weights[7];
216 float radial_gaussian_weight = exp(-M_PI * disk_point_length_squared) / sector_weights_sum;
217
218 /* Load the color of the pixel and its mirrored pixel and compute their square. */
219 float4 upper_color = float4(
220 inputs[0]->get_elem(clamp(it.x + i, 0, inputs[0]->get_width() - 1),
221 clamp(it.y + j, 0, inputs[0]->get_height() - 1)));
222 float4 lower_color = float4(
223 inputs[0]->get_elem(clamp(it.x - i, 0, inputs[0]->get_width() - 1),
224 clamp(it.y - j, 0, inputs[0]->get_height() - 1)));
225 float4 upper_color_squared = upper_color * upper_color;
226 float4 lower_color_squared = lower_color * lower_color;
227
228 for (int k = 0; k < number_of_sectors; k++) {
229 float weight = sector_weights[k] * radial_gaussian_weight;
230
231 /* Accumulate the pixel to each of the sectors multiplied by the sector weight. */
232 int upper_index = k;
233 sum_of_weights_of_sectors[upper_index] += weight;
234 weighted_mean_of_color_of_sectors[upper_index] += upper_color * weight;
235 weighted_mean_of_squared_color_of_sectors[upper_index] += upper_color_squared * weight;
236
237 /* Accumulate the mirrored pixel to each of the sectors multiplied by the sector weight.
238 */
239 int lower_index = (k + number_of_sectors / 2) % number_of_sectors;
240 sum_of_weights_of_sectors[lower_index] += weight;
241 weighted_mean_of_color_of_sectors[lower_index] += lower_color * weight;
242 weighted_mean_of_squared_color_of_sectors[lower_index] += lower_color_squared * weight;
243 }
244 }
245 }
246
247 /* Compute the weighted sum of mean of sectors, such that sectors with lower standard deviation
248 * gets more significant weight than sectors with higher standard deviation. */
249 float sum_of_weights = 0.0f;
250 float4 weighted_sum = float4(0.0f);
251 for (int i = 0; i < number_of_sectors; i++) {
252 weighted_mean_of_color_of_sectors[i] /= sum_of_weights_of_sectors[i];
253 weighted_mean_of_squared_color_of_sectors[i] /= sum_of_weights_of_sectors[i];
254
255 float4 color_mean = weighted_mean_of_color_of_sectors[i];
256 float4 squared_color_mean = weighted_mean_of_squared_color_of_sectors[i];
257 float4 color_variance = abs(squared_color_mean - color_mean * color_mean);
258
259 float standard_deviation = dot(sqrt(color_variance.xyz()), float3(1.0));
260
261 /* Compute the sector weight based on the weight function introduced in section "3.3.1
262 * Single-scale Filtering" of the multi-scale paper. Use a threshold of 0.02 to avoid zero
263 * division and avoid artifacts in homogeneous regions as demonstrated in the paper. */
264 float weight = 1.0f / pow(max(0.02f, standard_deviation), get_sharpness());
265
266 sum_of_weights += weight;
267 weighted_sum += color_mean * weight;
268 }
269
270 /* Fallback to the original color if all sector weights are zero due to very high standard
271 * deviation and sharpness. */
272 if (sum_of_weights == 0.0f) {
273 weighted_sum = center_color;
274 }
275 else {
276 weighted_sum /= sum_of_weights;
277 }
278
279 copy_v4_v4(it.out, weighted_sum);
280 }
281}
282
283/* The sharpness controls the sharpness of the transitions between the kuwahara sectors, which is
284 * controlled by the weighting function pow(standard_deviation, -sharpness) as can be seen in the
285 * compute function. The transition is completely smooth when the sharpness is zero and completely
286 * sharp when it is infinity. But realistically, the sharpness doesn't change much beyond the value
287 * of 16 due to its exponential nature, so we just assume a maximum sharpness of 16.
288 *
289 * The stored sharpness is in the range [0, 1], so we multiply by 16 to get it in the range
290 * [0, 16], however, we also square it before multiplication to slow down the rate of change near
291 * zero to counter its exponential nature for more intuitive user control. */
296
297/* The eccentricity controls how much the image anisotropy affects the eccentricity of the
298 * kuwahara sectors, which is controlled by the following factor that gets multiplied to the
299 * radius to get the ellipse width and divides the radius to get the ellipse height:
300 *
301 * (eccentricity + anisotropy) / eccentricity
302 *
303 * Since the anisotropy is in the [0, 1] range, the factor tends to 1 as the eccentricity tends
304 * to infinity and tends to infinity when the eccentricity tends to zero. The stored eccentricity
305 * is in the range [0, 2], we map that to the range [infinity, 0.5] by taking the reciprocal,
306 * satisfying the aforementioned limits. The upper limit doubles the computed default
307 * eccentricity, which users can use to enhance the directionality of the filter. Instead of
308 * actual infinity, we just use an eccentricity of 1 / 0.01 since the result is very similar to
309 * that of infinity. */
311{
312 return 1.0f / math::max(0.01f, eccentricity_);
313}
314
316{
317 sharpness_ = sharpness;
318}
319
321{
322 eccentricity_ = eccentricity;
323}
324
325} // namespace blender::compositor
sqrt(x)+1/max(0
#define M_SQRT1_2
#define M_PI
MINLINE void copy_v4_v4(float r[4], const float a[4])
SIMD_FORCE_INLINE btScalar length() const
Return the length of the vector.
Definition btVector3.h:257
void update_memory_buffer_partial(MemoryBuffer *output, const rcti &area, Span< MemoryBuffer * > inputs) override
a MemoryBuffer contains access to the data
void add_output_socket(DataType datatype)
void add_input_socket(DataType datatype, ResizeMode resize_mode=ResizeMode::Center)
pow(value.r - subtrahend, 2.0)") .do_static_compilation(true)
ccl_device_inline float3 exp(float3 v)
ccl_device_inline float3 ceil(const float3 a)
ccl_device_inline float3 cos(float3 v)
typename BuffersIteratorBuilder< T >::Iterator BuffersIterator
T max(const T &a, const T &b)
MatBase< float, 2, 2 > float2x2
VecBase< float, 4 > float4
VecBase< int32_t, 2 > int2
VecBase< float, 2 > float2
VecBase< T, 3 > xyz() const
float max
ccl_device_inline int abs(int x)
Definition util/math.h:120
ccl_device_inline int clamp(int a, int mn, int mx)
Definition util/math.h:379