VSE: add Cubic Mitchell filtering, rename previous cubic to Cubic BSpline #117517

Merged
Aras Pranckevicius merged 1 commits from aras_p/blender:vse-mitchell into main 2024-01-26 11:57:28 +01:00
10 changed files with 230 additions and 43 deletions

View File

@ -203,6 +203,27 @@ void interpolate_bilinear_wrap_fl(const float *buffer,
void interpolate_cubic_bspline_fl(
const float *buffer, float *output, int width, int height, int components, float u, float v);
/**
* Cubic Mitchell sampling.
*
* Takes 4x4 image samples at floor(u,v)-1 .. floor(u,v)+2, and blends them
* based on fractional parts of u,v. Uses Mitchell-Netravali filter (B=C=1/3),
* which has a good compromise between blur and ringing.
* Samples outside the image are clamped to texels at image edge.
*
* Note that you probably want to subtract 0.5 from u,v before this function,
* to get proper filtering.
*/
[[nodiscard]] uchar4 interpolate_cubic_mitchell_byte(
const uchar *buffer, int width, int height, float u, float v);
[[nodiscard]] float4 interpolate_cubic_mitchell_fl(
const float *buffer, int width, int height, float u, float v);
void interpolate_cubic_mitchell_fl(
const float *buffer, float *output, int width, int height, int components, float u, float v);
} // namespace blender::math
#define EWA_MAXIDX 255

View File

@ -10,24 +10,41 @@
#include <cstring>
#include "BLI_math_base.h"
#include "BLI_math_base.hh"
#include "BLI_math_interp.hh"
#include "BLI_math_vector.h"
#include "BLI_math_vector_types.hh"
#include "BLI_simd.h"
#include "BLI_strict_flags.h"
/* Cubic B-Spline coefficients. f is offset from texel center in pixel space.
* This is Mitchell-Netravali filter with B=1, C=0 parameters. */
static blender::float4 cubic_bspline_coefficients(float f)
enum class eCubicFilter {
BSpline,
Mitchell,
};
/* Calculate cubic filter coefficients, for samples at -1,0,+1,+2.
* f is 0..1 offset from texel center in pixel space. */
template<enum eCubicFilter filter> static blender::float4 cubic_filter_coefficients(float f)
{
float f2 = f * f;
float f3 = f2 * f;
float w3 = f3 / 6.0f;
float w0 = -w3 + f2 * 0.5f - f * 0.5f + 1.0f / 6.0f;
float w1 = f3 * 0.5f - f2 * 1.0f + 2.0f / 3.0f;
float w2 = 1.0f - w0 - w1 - w3;
return blender::float4(w0, w1, w2, w3);
if constexpr (filter == eCubicFilter::BSpline) {
/* Cubic B-Spline (Mitchell-Netravali filter with B=1, C=0 parameters). */
float w3 = f3 * (1.0f / 6.0f);
float w0 = -w3 + f2 * 0.5f - f * 0.5f + 1.0f / 6.0f;
float w1 = f3 * 0.5f - f2 * 1.0f + 2.0f / 3.0f;
float w2 = 1.0f - w0 - w1 - w3;
return blender::float4(w0, w1, w2, w3);
}
else if constexpr (filter == eCubicFilter::Mitchell) {
/* Cubic Mitchell-Netravali filter with B=1/3, C=1/3 parameters. */
float w0 = -7.0f / 18.0f * f3 + 5.0f / 6.0f * f2 - 0.5f * f + 1.0f / 18.0f;
float w1 = 7.0f / 6.0f * f3 - 2.0f * f2 + 8.0f / 9.0f;
float w2 = -7.0f / 6.0f * f3 + 3.0f / 2.0f * f2 + 0.5f * f + 1.0f / 18.0f;
float w3 = 7.0f / 18.0f * f3 - 1.0f / 3.0f * f2;
return blender::float4(w0, w1, w2, w3);
}
}
#if BLI_HAVE_SSE2
@ -51,6 +68,7 @@ BLI_INLINE __m128 floor_simd(__m128 v)
return v_floor;
}
template<eCubicFilter filter>
BLI_INLINE void bicubic_interpolation_uchar_simd(
const uchar *src_buffer, uchar *output, int width, int height, float u, float v)
{
@ -72,8 +90,8 @@ BLI_INLINE void bicubic_interpolation_uchar_simd(
__m128 frac_uv = _mm_sub_ps(uv, uv_floor);
/* Calculate pixel weights. */
blender::float4 wx = cubic_bspline_coefficients(_mm_cvtss_f32(frac_uv));
blender::float4 wy = cubic_bspline_coefficients(
blender::float4 wx = cubic_filter_coefficients<filter>(_mm_cvtss_f32(frac_uv));
blender::float4 wy = cubic_filter_coefficients<filter>(
_mm_cvtss_f32(_mm_shuffle_ps(frac_uv, frac_uv, 1)));
/* Read 4x4 source pixels and blend them. */
@ -102,7 +120,8 @@ BLI_INLINE void bicubic_interpolation_uchar_simd(
}
/* Pack and write to destination: pack to 16 bit signed, then to 8 bit
* unsigned, then write resulting 32-bit value. */
* unsigned, then write resulting 32-bit value. This will clamp
* out of range values too. */
out = _mm_add_ps(out, _mm_set1_ps(0.5f));
__m128i rgba32 = _mm_cvttps_epi32(out);
__m128i rgba16 = _mm_packs_epi32(rgba32, _mm_setzero_si128());
@ -111,7 +130,7 @@ BLI_INLINE void bicubic_interpolation_uchar_simd(
}
#endif /* BLI_HAVE_SSE2 */
template<typename T>
template<typename T, eCubicFilter filter>
static void bicubic_interpolation(
const T *src_buffer, T *output, int width, int height, int components, float u, float v)
{
@ -122,7 +141,7 @@ static void bicubic_interpolation(
#if BLI_HAVE_SSE2
if constexpr (std::is_same_v<T, uchar>) {
if (components == 4) {
bicubic_interpolation_uchar_simd(src_buffer, output, width, height, u, v);
bicubic_interpolation_uchar_simd<filter>(src_buffer, output, width, height, u, v);
return;
}
}
@ -143,8 +162,8 @@ static void bicubic_interpolation(
float4 out{0.0f};
/* Calculate pixel weights. */
float4 wx = cubic_bspline_coefficients(frac_u);
float4 wy = cubic_bspline_coefficients(frac_v);
float4 wx = cubic_filter_coefficients<filter>(frac_u);
float4 wy = cubic_filter_coefficients<filter>(frac_v);
/* Read 4x4 source pixels and blend them. */
for (int n = 0; n < 4; n++) {
@ -175,6 +194,16 @@ static void bicubic_interpolation(
}
}
/* Mitchell filter has negative lobes; prevent output from going out of range. */
if constexpr (filter == eCubicFilter::Mitchell) {
for (int i = 0; i < components; i++) {
out[i] = math::max(out[i], 0.0f);
if constexpr (std::is_same_v<T, uchar>) {
out[i] = math::min(out[i], 255.0f);
aras_p marked this conversation as resolved
Review

Not very obvious why negative lobe could lead to values above 255.

Not very obvious why negative lobe could lead to values above 255.
}
}
}
/* Write result. */
if constexpr (std::is_same_v<T, float>) {
if (components == 1) {
@ -551,21 +580,44 @@ float4 interpolate_bilinear_wrap_fl(const float *buffer, int width, int height,
uchar4 interpolate_cubic_bspline_byte(const uchar *buffer, int width, int height, float u, float v)
{
uchar4 res;
bicubic_interpolation<uchar>(buffer, res, width, height, 4, u, v);
bicubic_interpolation<uchar, eCubicFilter::BSpline>(buffer, res, width, height, 4, u, v);
return res;
}
float4 interpolate_cubic_bspline_fl(const float *buffer, int width, int height, float u, float v)
{
float4 res;
bicubic_interpolation<float>(buffer, res, width, height, 4, u, v);
bicubic_interpolation<float, eCubicFilter::BSpline>(buffer, res, width, height, 4, u, v);
return res;
}
void interpolate_cubic_bspline_fl(
const float *buffer, float *output, int width, int height, int components, float u, float v)
{
bicubic_interpolation<float>(buffer, output, width, height, components, u, v);
bicubic_interpolation<float, eCubicFilter::BSpline>(
buffer, output, width, height, components, u, v);
}
uchar4 interpolate_cubic_mitchell_byte(
const uchar *buffer, int width, int height, float u, float v)
{
uchar4 res;
bicubic_interpolation<uchar, eCubicFilter::Mitchell>(buffer, res, width, height, 4, u, v);
return res;
}
float4 interpolate_cubic_mitchell_fl(const float *buffer, int width, int height, float u, float v)
{
float4 res;
bicubic_interpolation<float, eCubicFilter::Mitchell>(buffer, res, width, height, 4, u, v);
return res;
}
void interpolate_cubic_mitchell_fl(
const float *buffer, float *output, int width, int height, int components, float u, float v)
{
bicubic_interpolation<float, eCubicFilter::Mitchell>(
buffer, output, width, height, components, u, v);
}
} // namespace blender::math

View File

@ -256,3 +256,65 @@ TEST(math_interp, CubicBSplineCharFullyOutsideImage)
res = interpolate_cubic_bspline_byte(image_char[0][0], image_width, image_height, 0, 500.0f);
EXPECT_EQ(exp, res);
}
TEST(math_interp, CubicMitchellCharExactSamples)
{
uchar4 res;
uchar4 exp1 = {72, 101, 140, 223};
res = interpolate_cubic_mitchell_byte(image_char[0][0], image_width, image_height, 1.0f, 2.0f);
EXPECT_EQ(int4(exp1), int4(res));
uchar4 exp2 = {233, 162, 99, 37};
res = interpolate_cubic_mitchell_byte(image_char[0][0], image_width, image_height, 2.0f, 0.0f);
EXPECT_EQ(int4(exp2), int4(res));
}
TEST(math_interp, CubicMitchellCharSamples)
{
uchar4 res;
uchar4 exp1 = {135, 132, 130, 127};
res = interpolate_cubic_mitchell_byte(
image_char[0][0], image_width, image_height, 1.25f, 0.625f);
EXPECT_EQ(int4(exp1), int4(res));
uchar4 exp2 = {216, 189, 167, 143};
res = interpolate_cubic_mitchell_byte(image_char[0][0], image_width, image_height, 1.4f, 0.1f);
EXPECT_EQ(int4(exp2), int4(res));
}
TEST(math_interp, CubicMitchellFloatSamples)
{
float4 res;
float4 exp1 = {134.5659f, 131.91309f, 130.17685f, 126.66989f};
res = interpolate_cubic_mitchell_fl(image_fl[0][0], image_width, image_height, 1.25f, 0.625f);
EXPECT_V4_NEAR(exp1, res, float_tolerance);
float4 exp2 = {216.27115f, 189.30673f, 166.93599f, 143.31964f};
res = interpolate_cubic_mitchell_fl(image_fl[0][0], image_width, image_height, 1.4f, 0.1f);
EXPECT_V4_NEAR(exp2, res, float_tolerance);
}
TEST(math_interp, CubicMitchellCharPartiallyOutsideImage)
{
uchar4 res;
uchar4 exp1 = {0, 0, 0, 0};
res = interpolate_cubic_mitchell_byte(image_char[0][0], image_width, image_height, -0.5f, 2.0f);
EXPECT_EQ(int4(exp1), int4(res));
uchar4 exp2 = {88, 116, 151, 228};
res = interpolate_cubic_mitchell_byte(image_char[0][0], image_width, image_height, 1.25f, 2.9f);
EXPECT_EQ(int4(exp2), int4(res));
uchar4 exp3 = {239, 159, 89, 19};
res = interpolate_cubic_mitchell_byte(image_char[0][0], image_width, image_height, 2.2f, -0.1f);
EXPECT_EQ(int4(exp3), int4(res));
}
TEST(math_interp, CubicMitchellFloatPartiallyOutsideImage)
{
float4 res;
float4 exp1 = {0, 0, 0, 0};
res = interpolate_cubic_mitchell_fl(image_fl[0][0], image_width, image_height, -0.5f, 2.0f);
EXPECT_V4_NEAR(exp1, res, float_tolerance);
float4 exp2 = {87.98676f, 115.63634f, 151.13014f, 228.19823f};
res = interpolate_cubic_mitchell_fl(image_fl[0][0], image_width, image_height, 1.25f, 2.9f);
EXPECT_V4_NEAR(exp2, res, float_tolerance);
float4 exp3 = {238.6136f, 158.58293f, 88.55761f, 18.53225f};
res = interpolate_cubic_mitchell_fl(image_fl[0][0], image_width, image_height, 2.2f, -0.1f);
EXPECT_V4_NEAR(exp3, res, float_tolerance);
}

View File

@ -264,7 +264,8 @@ void IMB_rectblend_threaded(ImBuf *dbuf,
enum eIMBInterpolationFilterMode {
IMB_FILTER_NEAREST,
IMB_FILTER_BILINEAR,
IMB_FILTER_BICUBIC,
IMB_FILTER_CUBIC_BSPLINE,
IMB_FILTER_CUBIC_MITCHELL,
};
/**

View File

@ -91,6 +91,16 @@ inline void interpolate_cubic_bspline_fl(const ImBuf *in, float output[4], float
memcpy(output, &col, sizeof(col));
}
[[nodiscard]] inline uchar4 interpolate_cubic_mitchell_byte(const ImBuf *in, float u, float v)
{
return math::interpolate_cubic_mitchell_byte(in->byte_buffer.data, in->x, in->y, u, v);
}
inline void interpolate_cubic_mitchell_byte(const ImBuf *in, uchar output[4], float u, float v)
{
uchar4 col = math::interpolate_cubic_mitchell_byte(in->byte_buffer.data, in->x, in->y, u, v);
memcpy(output, &col, sizeof(col));
}
} // namespace blender::imbuf
/**

View File

@ -146,10 +146,10 @@ static void sample_image(const ImBuf *source, float u, float v, T *r_sample)
u = wrap_uv(u, source->x);
v = wrap_uv(v, source->y);
}
/* BLI_bilinear_interpolation functions use `floor(uv)` and `floor(uv)+1`
/* Bilinear/cubic interpolation functions use `floor(uv)` and `floor(uv)+1`
* texels. For proper mapping between pixel and texel spaces, need to
* subtract 0.5. Same for bicubic. */
if constexpr (ELEM(Filter, IMB_FILTER_BILINEAR, IMB_FILTER_BICUBIC)) {
* subtract 0.5. */
if constexpr (Filter != IMB_FILTER_NEAREST) {
u -= 0.5f;
v -= 0.5f;
}
@ -185,14 +185,24 @@ static void sample_image(const ImBuf *source, float u, float v, T *r_sample)
math::interpolate_nearest_fl(
source->float_buffer.data, r_sample, source->x, source->y, NumChannels, u, v);
}
else if constexpr (Filter == IMB_FILTER_BICUBIC && std::is_same_v<T, float>) {
else if constexpr (Filter == IMB_FILTER_CUBIC_BSPLINE && std::is_same_v<T, float>) {
math::interpolate_cubic_bspline_fl(
source->float_buffer.data, r_sample, source->x, source->y, NumChannels, u, v);
}
else if constexpr (Filter == IMB_FILTER_BICUBIC && std::is_same_v<T, uchar> && NumChannels == 4)
else if constexpr (Filter == IMB_FILTER_CUBIC_BSPLINE && std::is_same_v<T, uchar> &&
NumChannels == 4)
{
interpolate_cubic_bspline_byte(source, r_sample, u, v);
}
else if constexpr (Filter == IMB_FILTER_CUBIC_MITCHELL && std::is_same_v<T, float>) {
math::interpolate_cubic_mitchell_fl(
source->float_buffer.data, r_sample, source->x, source->y, NumChannels, u, v);
}
else if constexpr (Filter == IMB_FILTER_CUBIC_MITCHELL && std::is_same_v<T, uchar> &&
NumChannels == 4)
{
interpolate_cubic_mitchell_byte(source, r_sample, u, v);
}
else {
/* Unsupported sampler. */
BLI_assert_unreachable();
@ -385,8 +395,11 @@ void IMB_transform(const ImBuf *src,
else if (filter == IMB_FILTER_BILINEAR) {
transform_scanlines_filter<IMB_FILTER_BILINEAR>(ctx, y_range);
}
else if (filter == IMB_FILTER_BICUBIC) {
transform_scanlines_filter<IMB_FILTER_BICUBIC>(ctx, y_range);
else if (filter == IMB_FILTER_CUBIC_BSPLINE) {
Review

This should probably become a switch statement.

This should probably become a `switch` statement.
transform_scanlines_filter<IMB_FILTER_CUBIC_BSPLINE>(ctx, y_range);
}
else if (filter == IMB_FILTER_CUBIC_MITCHELL) {
transform_scanlines_filter<IMB_FILTER_CUBIC_MITCHELL>(ctx, y_range);
}
});
}

View File

@ -87,9 +87,9 @@ TEST(imbuf_transform, bilinear_2x_smaller)
IMB_freeImBuf(res);
}
TEST(imbuf_transform, bicubic_2x_smaller)
TEST(imbuf_transform, cubic_bspline_2x_smaller)
{
ImBuf *res = transform_2x_smaller(IMB_FILTER_BICUBIC, 1);
ImBuf *res = transform_2x_smaller(IMB_FILTER_CUBIC_BSPLINE, 1);
const ColorTheme4b *got = reinterpret_cast<ColorTheme4b *>(res->byte_buffer.data);
EXPECT_EQ(got[0], ColorTheme4b(189, 126, 62, 250));
EXPECT_EQ(got[1], ColorTheme4b(134, 57, 33, 26));
@ -97,16 +97,26 @@ TEST(imbuf_transform, bicubic_2x_smaller)
IMB_freeImBuf(res);
}
TEST(imbuf_transform, bicubic_fractional_larger)
TEST(imbuf_transform, cubic_mitchell_2x_smaller)
{
ImBuf *res = transform_fractional_larger(IMB_FILTER_BICUBIC, 1);
ImBuf *res = transform_2x_smaller(IMB_FILTER_CUBIC_MITCHELL, 1);
const ColorTheme4b *got = reinterpret_cast<ColorTheme4b *>(res->byte_buffer.data);
EXPECT_EQ(got[0 + 0 * res->x], ColorTheme4b(35, 11, 1, 255));
EXPECT_EQ(got[1 + 0 * res->x], ColorTheme4b(131, 12, 6, 250));
EXPECT_EQ(got[7 + 0 * res->x], ColorTheme4b(54, 93, 19, 249));
EXPECT_EQ(got[2 + 2 * res->x], ColorTheme4b(206, 70, 56, 192));
EXPECT_EQ(got[3 + 2 * res->x], ColorTheme4b(165, 60, 42, 78));
EXPECT_EQ(got[8 + 6 * res->x], ColorTheme4b(57, 1, 90, 252));
EXPECT_EQ(got[0], ColorTheme4b(195, 130, 67, 255));
EXPECT_EQ(got[1], ColorTheme4b(132, 51, 28, 0));
EXPECT_EQ(got[2], ColorTheme4b(52, 52, 48, 255));
IMB_freeImBuf(res);
}
TEST(imbuf_transform, cubic_mitchell_fractional_larger)
{
ImBuf *res = transform_fractional_larger(IMB_FILTER_CUBIC_MITCHELL, 1);
const ColorTheme4b *got = reinterpret_cast<ColorTheme4b *>(res->byte_buffer.data);
EXPECT_EQ(got[0 + 0 * res->x], ColorTheme4b(0, 0, 0, 255));
EXPECT_EQ(got[1 + 0 * res->x], ColorTheme4b(127, 0, 0, 255));
EXPECT_EQ(got[7 + 0 * res->x], ColorTheme4b(49, 109, 13, 255));
EXPECT_EQ(got[2 + 2 * res->x], ColorTheme4b(236, 53, 50, 215));
EXPECT_EQ(got[3 + 2 * res->x], ColorTheme4b(155, 55, 35, 54));
EXPECT_EQ(got[8 + 6 * res->x], ColorTheme4b(57, 0, 98, 252));
IMB_freeImBuf(res);
}

View File

@ -839,7 +839,8 @@ enum {
SEQ_TRANSFORM_FILTER_NEAREST = 0,
SEQ_TRANSFORM_FILTER_BILINEAR = 1,
SEQ_TRANSFORM_FILTER_NEAREST_3x3 = 2,
SEQ_TRANSFORM_FILTER_BICUBIC = 3,
SEQ_TRANSFORM_FILTER_CUBIC_BSPLINE = 3,
SEQ_TRANSFORM_FILTER_CUBIC_MITCHELL = 4,
};
typedef enum eSeqChannelFlag {

View File

@ -1709,14 +1709,28 @@ static void rna_def_strip_crop(BlenderRNA *brna)
}
static const EnumPropertyItem transform_filter_items[] = {
{SEQ_TRANSFORM_FILTER_NEAREST, "NEAREST", 0, "Nearest", ""},
{SEQ_TRANSFORM_FILTER_BILINEAR, "BILINEAR", 0, "Bilinear", ""},
{SEQ_TRANSFORM_FILTER_BICUBIC, "BICUBIC", 0, "Bicubic", ""},
{SEQ_TRANSFORM_FILTER_NEAREST, "NEAREST", 0, "Nearest", "Use nearest sample"},
{SEQ_TRANSFORM_FILTER_BILINEAR,
"BILINEAR",
0,
"Bilinear",
"Interpolate between 2" BLI_STR_UTF8_MULTIPLICATION_SIGN "2 samples"},
{SEQ_TRANSFORM_FILTER_CUBIC_MITCHELL,
"CUBIC_MITCHELL",
0,
"Cubic Mitchell",
"Cubic Mitchell filter on 4" BLI_STR_UTF8_MULTIPLICATION_SIGN "4 samples"},
{SEQ_TRANSFORM_FILTER_CUBIC_BSPLINE,
"CUBIC_BSPLINE",
0,
"Cubic B-Spline",
"Cubic B-Spline filter (blurry but no ringing) on 4" BLI_STR_UTF8_MULTIPLICATION_SIGN
"4 samples"},
{SEQ_TRANSFORM_FILTER_NEAREST_3x3,
"SUBSAMPLING_3x3",
0,
"Subsampling (3" BLI_STR_UTF8_MULTIPLICATION_SIGN "3)",
"Use nearest with 3" BLI_STR_UTF8_MULTIPLICATION_SIGN "3 subsamples during rendering"},
"Use nearest with 3" BLI_STR_UTF8_MULTIPLICATION_SIGN "3 subsamples"},
{0, nullptr, 0, nullptr, nullptr},
};

View File

@ -545,8 +545,11 @@ static void sequencer_preprocess_transform_crop(
case SEQ_TRANSFORM_FILTER_BILINEAR:
filter = IMB_FILTER_BILINEAR;
break;
case SEQ_TRANSFORM_FILTER_BICUBIC:
filter = IMB_FILTER_BICUBIC;
case SEQ_TRANSFORM_FILTER_CUBIC_BSPLINE:
filter = IMB_FILTER_CUBIC_BSPLINE;
break;
case SEQ_TRANSFORM_FILTER_CUBIC_MITCHELL:
filter = IMB_FILTER_CUBIC_MITCHELL;
break;
case SEQ_TRANSFORM_FILTER_NEAREST_3x3:
filter = IMB_FILTER_NEAREST;