VSE: replace Subsampled3x3 filter by a general Box filter #117584

Merged
Aras Pranckevicius merged 1 commits from aras_p/blender:vse_box_filter into main 2024-01-29 18:41:40 +01:00
7 changed files with 66 additions and 82 deletions

View File

@ -616,7 +616,6 @@ template<typename TextureMethod> class ScreenSpaceDrawingMode : public AbstractD
&texture_buffer, &texture_buffer,
transform_mode, transform_mode,
IMB_FILTER_NEAREST, IMB_FILTER_NEAREST,
1,
uv_to_texel.ptr(), uv_to_texel.ptr(),
crop_rect_ptr); crop_rect_ptr);
} }

View File

@ -266,6 +266,7 @@ enum eIMBInterpolationFilterMode {
IMB_FILTER_BILINEAR, IMB_FILTER_BILINEAR,
IMB_FILTER_CUBIC_BSPLINE, IMB_FILTER_CUBIC_BSPLINE,
IMB_FILTER_CUBIC_MITCHELL, IMB_FILTER_CUBIC_MITCHELL,
IMB_FILTER_BOX,
}; };
/** /**
@ -651,8 +652,6 @@ enum eIMBTransformMode {
* - Only one data type buffer will be used (rect_float has priority over rect) * - Only one data type buffer will be used (rect_float has priority over rect)
* \param mode: Cropping/Wrap repeat effect to apply during transformation. * \param mode: Cropping/Wrap repeat effect to apply during transformation.
* \param filter: Interpolation to use during sampling. * \param filter: Interpolation to use during sampling.
* \param num_subsamples: Number of subsamples to use. Increasing this would improve the quality,
* but reduces the performance.
* \param transform_matrix: Transformation matrix to use. * \param transform_matrix: Transformation matrix to use.
* The given matrix should transform between dst pixel space to src pixel space. * The given matrix should transform between dst pixel space to src pixel space.
* One unit is one pixel. * One unit is one pixel.
@ -667,7 +666,6 @@ void IMB_transform(const ImBuf *src,
ImBuf *dst, ImBuf *dst,
eIMBTransformMode mode, eIMBTransformMode mode,
eIMBInterpolationFilterMode filter, eIMBInterpolationFilterMode filter,
const int num_subsamples,
const float transform_matrix[4][4], const float transform_matrix[4][4],
const rctf *src_crop); const rctf *src_crop);

View File

@ -15,7 +15,6 @@
#include "BLI_math_vector.h" #include "BLI_math_vector.h"
#include "BLI_rect.h" #include "BLI_rect.h"
#include "BLI_task.hh" #include "BLI_task.hh"
#include "BLI_vector.hh"
#include "IMB_imbuf.hh" #include "IMB_imbuf.hh"
#include "IMB_interp.hh" #include "IMB_interp.hh"
@ -39,42 +38,21 @@ struct TransformContext {
/* Source UV step delta, when moving along one destination pixel in Y axis. */ /* Source UV step delta, when moving along one destination pixel in Y axis. */
float2 add_y; float2 add_y;
/* Per-subsample source image delta UVs. */
Vector<float2, 9> subsampling_deltas;
IndexRange dst_region_x_range; IndexRange dst_region_x_range;
IndexRange dst_region_y_range; IndexRange dst_region_y_range;
/* Cropping region in source image pixel space. */ /* Cropping region in source image pixel space. */
rctf src_crop; rctf src_crop;
void init(const float4x4 &transform_matrix, const int num_subsamples, const bool has_source_crop) void init(const float4x4 &transform_matrix, const bool has_source_crop)
{ {
start_uv = transform_matrix.location().xy(); start_uv = transform_matrix.location().xy();
add_x = transform_matrix.x_axis().xy(); add_x = transform_matrix.x_axis().xy();
add_y = transform_matrix.y_axis().xy(); add_y = transform_matrix.y_axis().xy();
init_subsampling(num_subsamples);
init_destination_region(transform_matrix, has_source_crop); init_destination_region(transform_matrix, has_source_crop);
} }
private: private:
void init_subsampling(const int num_subsamples)
{
float2 subsample_add_x = add_x / num_subsamples;
float2 subsample_add_y = add_y / num_subsamples;
float2 offset_x = -add_x * 0.5f + subsample_add_x * 0.5f;
float2 offset_y = -add_y * 0.5f + subsample_add_y * 0.5f;
for (int y : IndexRange(0, num_subsamples)) {
for (int x : IndexRange(0, num_subsamples)) {
float2 delta_uv = offset_x + offset_y;
delta_uv += x * subsample_add_x;
delta_uv += y * subsample_add_y;
subsampling_deltas.append(delta_uv);
}
}
}
void init_destination_region(const float4x4 &transform_matrix, const bool has_source_crop) void init_destination_region(const float4x4 &transform_matrix, const bool has_source_crop)
{ {
if (!has_source_crop) { if (!has_source_crop) {
@ -265,27 +243,47 @@ template<eIMBInterpolationFilterMode Filter,
bool WrapUV> bool WrapUV>
static void process_scanlines(const TransformContext &ctx, IndexRange y_range) static void process_scanlines(const TransformContext &ctx, IndexRange y_range)
{ {
/* Note: sample at pixel center for proper filtering. */ if constexpr (Filter == IMB_FILTER_BOX) {
float2 uv_start = ctx.start_uv + ctx.add_x * 0.5f + ctx.add_y * 0.5f;
if (ctx.subsampling_deltas.size() > 1) {
/* Multiple samples per pixel: accumulate them pre-multiplied, /* Multiple samples per pixel: accumulate them pre-multiplied,
* divide by sample count and write out (un-pre-multiplying if writing out * divide by sample count and write out (un-pre-multiplying if writing out
* to byte image). */ * to byte image).
const float inv_count = 1.0f / ctx.subsampling_deltas.size(); *
* Do a box filter: for each destination pixel, accumulate XxY samples from source,
* based on scaling factors (length of X/Y pixel steps). Use at least 2 samples
* along each direction, so that in case of rotation the resulting edges get
* some anti-aliasing, to match previous Subsampled3x3 filter behavior. The
* "at least 2" can be removed once/if transform edge anti-aliasing is implemented
* in general way for all filters. Use at most 100 samples along each direction,
* just as some way of clamping possible upper cost. Scaling something down by more
* than 100x should rarely if ever happen, worst case they will get some aliasing.
*/
float2 uv_start = ctx.start_uv;
int sub_count_x = int(math::clamp(roundf(math::length(ctx.add_x)), 2.0f, 100.0f));
int sub_count_y = int(math::clamp(roundf(math::length(ctx.add_y)), 2.0f, 100.0f));
const float inv_count = 1.0f / (sub_count_x * sub_count_y);
const float2 sub_step_x = ctx.add_x / sub_count_x;
const float2 sub_step_y = ctx.add_y / sub_count_y;
for (int yi : y_range) { for (int yi : y_range) {
T *output = init_pixel_pointer<T>(ctx.dst, ctx.dst_region_x_range.first(), yi); T *output = init_pixel_pointer<T>(ctx.dst, ctx.dst_region_x_range.first(), yi);
float2 uv_row = uv_start + yi * ctx.add_y; float2 uv_row = uv_start + yi * ctx.add_y;
for (int xi : ctx.dst_region_x_range) { for (int xi : ctx.dst_region_x_range) {
float2 uv = uv_row + xi * ctx.add_x; const float2 uv = uv_row + xi * ctx.add_x;
float sample[4] = {}; float sample[4] = {};
for (const float2 &delta_uv : ctx.subsampling_deltas) { for (int sub_y = 0; sub_y < sub_count_y; sub_y++) {
const float2 sub_uv = uv + delta_uv; for (int sub_x = 0; sub_x < sub_count_x; sub_x++) {
if (!CropSource || !should_discard(ctx, sub_uv)) { float2 delta = (sub_x + 0.5f) * sub_step_x + (sub_y + 0.5f) * sub_step_y;
T sub_sample[4]; float2 sub_uv = uv + delta;
sample_image<Filter, T, SrcChannels, WrapUV>(ctx.src, sub_uv.x, sub_uv.y, sub_sample); if (!CropSource || !should_discard(ctx, sub_uv)) {
add_subsample(sub_sample, sample); T sub_sample[4];
sample_image<eIMBInterpolationFilterMode::IMB_FILTER_NEAREST,
T,
SrcChannels,
WrapUV>(ctx.src, sub_uv.x, sub_uv.y, sub_sample);
add_subsample(sub_sample, sample);
}
} }
} }
@ -297,7 +295,8 @@ static void process_scanlines(const TransformContext &ctx, IndexRange y_range)
} }
} }
else { else {
/* One sample per pixel. */ /* One sample per pixel. Note: sample at pixel center for proper filtering. */
float2 uv_start = ctx.start_uv + ctx.add_x * 0.5f + ctx.add_y * 0.5f;
for (int yi : y_range) { for (int yi : y_range) {
T *output = init_pixel_pointer<T>(ctx.dst, ctx.dst_region_x_range.first(), yi); T *output = init_pixel_pointer<T>(ctx.dst, ctx.dst_region_x_range.first(), yi);
float2 uv_row = uv_start + yi * ctx.add_y; float2 uv_row = uv_start + yi * ctx.add_y;
@ -369,7 +368,6 @@ void IMB_transform(const ImBuf *src,
ImBuf *dst, ImBuf *dst,
const eIMBTransformMode mode, const eIMBTransformMode mode,
const eIMBInterpolationFilterMode filter, const eIMBInterpolationFilterMode filter,
const int num_subsamples,
const float transform_matrix[4][4], const float transform_matrix[4][4],
const rctf *src_crop) const rctf *src_crop)
{ {
@ -386,7 +384,7 @@ void IMB_transform(const ImBuf *src,
if (crop) { if (crop) {
ctx.src_crop = *src_crop; ctx.src_crop = *src_crop;
} }
ctx.init(blender::float4x4(transform_matrix), num_subsamples, crop); ctx.init(blender::float4x4(transform_matrix), crop);
threading::parallel_for(ctx.dst_region_y_range, 8, [&](IndexRange y_range) { threading::parallel_for(ctx.dst_region_y_range, 8, [&](IndexRange y_range) {
if (filter == IMB_FILTER_NEAREST) { if (filter == IMB_FILTER_NEAREST) {
@ -401,5 +399,8 @@ void IMB_transform(const ImBuf *src,
else if (filter == IMB_FILTER_CUBIC_MITCHELL) { else if (filter == IMB_FILTER_CUBIC_MITCHELL) {
transform_scanlines_filter<IMB_FILTER_CUBIC_MITCHELL>(ctx, y_range); transform_scanlines_filter<IMB_FILTER_CUBIC_MITCHELL>(ctx, y_range);
} }
else if (filter == IMB_FILTER_BOX) {
transform_scanlines_filter<IMB_FILTER_BOX>(ctx, y_range);
}
}); });
} }

View File

@ -37,29 +37,29 @@ static ImBuf *create_6x2_test_image()
return img; return img;
} }
static ImBuf *transform_2x_smaller(eIMBInterpolationFilterMode filter, int subsamples) static ImBuf *transform_2x_smaller(eIMBInterpolationFilterMode filter)
{ {
ImBuf *src = create_6x2_test_image(); ImBuf *src = create_6x2_test_image();
ImBuf *dst = IMB_allocImBuf(3, 1, 32, IB_rect); ImBuf *dst = IMB_allocImBuf(3, 1, 32, IB_rect);
float4x4 matrix = math::from_scale<float4x4>(float4(2.0f)); float4x4 matrix = math::from_scale<float4x4>(float4(2.0f));
IMB_transform(src, dst, IMB_TRANSFORM_MODE_REGULAR, filter, subsamples, matrix.ptr(), nullptr); IMB_transform(src, dst, IMB_TRANSFORM_MODE_REGULAR, filter, matrix.ptr(), nullptr);
IMB_freeImBuf(src); IMB_freeImBuf(src);
return dst; return dst;
} }
static ImBuf *transform_fractional_larger(eIMBInterpolationFilterMode filter, int subsamples) static ImBuf *transform_fractional_larger(eIMBInterpolationFilterMode filter)
{ {
ImBuf *src = create_6x2_test_image(); ImBuf *src = create_6x2_test_image();
ImBuf *dst = IMB_allocImBuf(9, 7, 32, IB_rect); ImBuf *dst = IMB_allocImBuf(9, 7, 32, IB_rect);
float4x4 matrix = math::from_scale<float4x4>(float4(6.0f / 9.0f, 2.0f / 7.0f, 1.0f, 1.0f)); float4x4 matrix = math::from_scale<float4x4>(float4(6.0f / 9.0f, 2.0f / 7.0f, 1.0f, 1.0f));
IMB_transform(src, dst, IMB_TRANSFORM_MODE_REGULAR, filter, subsamples, matrix.ptr(), nullptr); IMB_transform(src, dst, IMB_TRANSFORM_MODE_REGULAR, filter, matrix.ptr(), nullptr);
IMB_freeImBuf(src); IMB_freeImBuf(src);
return dst; return dst;
} }
TEST(imbuf_transform, nearest_2x_smaller) TEST(imbuf_transform, nearest_2x_smaller)
{ {
ImBuf *res = transform_2x_smaller(IMB_FILTER_NEAREST, 1); ImBuf *res = transform_2x_smaller(IMB_FILTER_NEAREST);
const ColorTheme4b *got = reinterpret_cast<ColorTheme4b *>(res->byte_buffer.data); const ColorTheme4b *got = reinterpret_cast<ColorTheme4b *>(res->byte_buffer.data);
EXPECT_EQ(got[0], ColorTheme4b(255, 255, 255, 255)); EXPECT_EQ(got[0], ColorTheme4b(255, 255, 255, 255));
EXPECT_EQ(got[1], ColorTheme4b(133, 55, 31, 19)); EXPECT_EQ(got[1], ColorTheme4b(133, 55, 31, 19));
@ -67,19 +67,20 @@ TEST(imbuf_transform, nearest_2x_smaller)
IMB_freeImBuf(res); IMB_freeImBuf(res);
} }
TEST(imbuf_transform, nearest_subsample3_2x_smaller) TEST(imbuf_transform, box_2x_smaller)
{ {
ImBuf *res = transform_2x_smaller(IMB_FILTER_NEAREST, 3); ImBuf *res = transform_2x_smaller(IMB_FILTER_BOX);
const ColorTheme4b *got = reinterpret_cast<ColorTheme4b *>(res->byte_buffer.data); const ColorTheme4b *got = reinterpret_cast<ColorTheme4b *>(res->byte_buffer.data);
EXPECT_EQ(got[0], ColorTheme4b(227, 170, 113, 255)); /* At 2x reduction should be same as bilinear, save for some rounding errors. */
EXPECT_EQ(got[1], ColorTheme4b(133, 55, 31, 17)); EXPECT_EQ(got[0], ColorTheme4b(191, 128, 64, 255));
EXPECT_EQ(got[2], ColorTheme4b(56, 22, 64, 253)); EXPECT_EQ(got[1], ColorTheme4b(133, 55, 31, 16));
EXPECT_EQ(got[2], ColorTheme4b(54, 50, 48, 254));
IMB_freeImBuf(res); IMB_freeImBuf(res);
} }
TEST(imbuf_transform, bilinear_2x_smaller) TEST(imbuf_transform, bilinear_2x_smaller)
{ {
ImBuf *res = transform_2x_smaller(IMB_FILTER_BILINEAR, 1); ImBuf *res = transform_2x_smaller(IMB_FILTER_BILINEAR);
const ColorTheme4b *got = reinterpret_cast<ColorTheme4b *>(res->byte_buffer.data); const ColorTheme4b *got = reinterpret_cast<ColorTheme4b *>(res->byte_buffer.data);
EXPECT_EQ(got[0], ColorTheme4b(191, 128, 64, 255)); EXPECT_EQ(got[0], ColorTheme4b(191, 128, 64, 255));
EXPECT_EQ(got[1], ColorTheme4b(133, 55, 31, 16)); EXPECT_EQ(got[1], ColorTheme4b(133, 55, 31, 16));
@ -89,7 +90,7 @@ TEST(imbuf_transform, bilinear_2x_smaller)
TEST(imbuf_transform, cubic_bspline_2x_smaller) TEST(imbuf_transform, cubic_bspline_2x_smaller)
{ {
ImBuf *res = transform_2x_smaller(IMB_FILTER_CUBIC_BSPLINE, 1); ImBuf *res = transform_2x_smaller(IMB_FILTER_CUBIC_BSPLINE);
const ColorTheme4b *got = reinterpret_cast<ColorTheme4b *>(res->byte_buffer.data); const ColorTheme4b *got = reinterpret_cast<ColorTheme4b *>(res->byte_buffer.data);
EXPECT_EQ(got[0], ColorTheme4b(189, 126, 62, 250)); EXPECT_EQ(got[0], ColorTheme4b(189, 126, 62, 250));
EXPECT_EQ(got[1], ColorTheme4b(134, 57, 33, 26)); EXPECT_EQ(got[1], ColorTheme4b(134, 57, 33, 26));
@ -99,7 +100,7 @@ TEST(imbuf_transform, cubic_bspline_2x_smaller)
TEST(imbuf_transform, cubic_mitchell_2x_smaller) TEST(imbuf_transform, cubic_mitchell_2x_smaller)
{ {
ImBuf *res = transform_2x_smaller(IMB_FILTER_CUBIC_MITCHELL, 1); ImBuf *res = transform_2x_smaller(IMB_FILTER_CUBIC_MITCHELL);
const ColorTheme4b *got = reinterpret_cast<ColorTheme4b *>(res->byte_buffer.data); const ColorTheme4b *got = reinterpret_cast<ColorTheme4b *>(res->byte_buffer.data);
EXPECT_EQ(got[0], ColorTheme4b(195, 130, 67, 255)); EXPECT_EQ(got[0], ColorTheme4b(195, 130, 67, 255));
EXPECT_EQ(got[1], ColorTheme4b(132, 51, 28, 0)); EXPECT_EQ(got[1], ColorTheme4b(132, 51, 28, 0));
@ -109,7 +110,7 @@ TEST(imbuf_transform, cubic_mitchell_2x_smaller)
TEST(imbuf_transform, cubic_mitchell_fractional_larger) TEST(imbuf_transform, cubic_mitchell_fractional_larger)
{ {
ImBuf *res = transform_fractional_larger(IMB_FILTER_CUBIC_MITCHELL, 1); ImBuf *res = transform_fractional_larger(IMB_FILTER_CUBIC_MITCHELL);
const ColorTheme4b *got = reinterpret_cast<ColorTheme4b *>(res->byte_buffer.data); 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[0 + 0 * res->x], ColorTheme4b(0, 0, 0, 255));
EXPECT_EQ(got[1 + 0 * res->x], ColorTheme4b(127, 0, 0, 255)); EXPECT_EQ(got[1 + 0 * res->x], ColorTheme4b(127, 0, 0, 255));
@ -138,8 +139,7 @@ TEST(imbuf_transform, nearest_very_large_scale)
ImBuf *res = IMB_allocImBuf(3841, 1, 32, IB_rect); ImBuf *res = IMB_allocImBuf(3841, 1, 32, IB_rect);
float4x4 matrix = math::from_loc_rot_scale<float4x4>( float4x4 matrix = math::from_loc_rot_scale<float4x4>(
float3(254, 0, 0), math::Quaternion::identity(), float3(3.0f / 3840.0f, 1, 1)); float3(254, 0, 0), math::Quaternion::identity(), float3(3.0f / 3840.0f, 1, 1));
IMB_transform( IMB_transform(src, res, IMB_TRANSFORM_MODE_REGULAR, IMB_FILTER_NEAREST, matrix.ptr(), nullptr);
src, res, IMB_TRANSFORM_MODE_REGULAR, IMB_FILTER_NEAREST, 1, matrix.ptr(), nullptr);
/* Check result: leftmost red, middle green, two rightmost pixels blue and black. /* Check result: leftmost red, middle green, two rightmost pixels blue and black.
* If the transform code internally does not have enough precision while stepping * If the transform code internally does not have enough precision while stepping

View File

@ -838,7 +838,7 @@ typedef enum SequenceColorTag {
enum { enum {
SEQ_TRANSFORM_FILTER_NEAREST = 0, SEQ_TRANSFORM_FILTER_NEAREST = 0,
SEQ_TRANSFORM_FILTER_BILINEAR = 1, SEQ_TRANSFORM_FILTER_BILINEAR = 1,
SEQ_TRANSFORM_FILTER_NEAREST_3x3 = 2, SEQ_TRANSFORM_FILTER_BOX = 2,
SEQ_TRANSFORM_FILTER_CUBIC_BSPLINE = 3, SEQ_TRANSFORM_FILTER_CUBIC_BSPLINE = 3,
SEQ_TRANSFORM_FILTER_CUBIC_MITCHELL = 4, SEQ_TRANSFORM_FILTER_CUBIC_MITCHELL = 4,
}; };

View File

@ -1726,11 +1726,11 @@ static const EnumPropertyItem transform_filter_items[] = {
"Cubic B-Spline", "Cubic B-Spline",
"Cubic B-Spline filter (blurry but no ringing) on 4" BLI_STR_UTF8_MULTIPLICATION_SIGN "Cubic B-Spline filter (blurry but no ringing) on 4" BLI_STR_UTF8_MULTIPLICATION_SIGN
"4 samples"}, "4 samples"},
{SEQ_TRANSFORM_FILTER_NEAREST_3x3, {SEQ_TRANSFORM_FILTER_BOX,
"SUBSAMPLING_3x3", "BOX",
0, 0,
"Subsampling (3" BLI_STR_UTF8_MULTIPLICATION_SIGN "3)", "Box",
"Use nearest with 3" BLI_STR_UTF8_MULTIPLICATION_SIGN "3 subsamples"}, "Averages source image samples that fall under destination pixel"},
{0, nullptr, 0, nullptr, nullptr}, {0, nullptr, 0, nullptr, nullptr},
}; };

View File

@ -465,14 +465,8 @@ static void sequencer_thumbnail_transform(ImBuf *in, ImBuf *out)
blender::float3{scale_x, scale_y, 1.0f}); blender::float3{scale_x, scale_y, 1.0f});
transform_pivot_set_m4(transform_matrix, pivot); transform_pivot_set_m4(transform_matrix, pivot);
invert_m4(transform_matrix); invert_m4(transform_matrix);
const int num_subsamples = 1; IMB_transform(
IMB_transform(in, in, out, IMB_TRANSFORM_MODE_REGULAR, IMB_FILTER_NEAREST, transform_matrix, nullptr);
out,
IMB_TRANSFORM_MODE_REGULAR,
IMB_FILTER_NEAREST,
num_subsamples,
transform_matrix,
nullptr);
} }
/* Check whether transform introduces transparent ares in the result (happens when the transformed /* Check whether transform introduces transparent ares in the result (happens when the transformed
@ -537,7 +531,6 @@ static void sequencer_preprocess_transform_crop(
const StripTransform *transform = seq->strip->transform; const StripTransform *transform = seq->strip->transform;
eIMBInterpolationFilterMode filter = IMB_FILTER_NEAREST; eIMBInterpolationFilterMode filter = IMB_FILTER_NEAREST;
int num_subsamples = 1;
switch (transform->filter) { switch (transform->filter) {
case SEQ_TRANSFORM_FILTER_NEAREST: case SEQ_TRANSFORM_FILTER_NEAREST:
filter = IMB_FILTER_NEAREST; filter = IMB_FILTER_NEAREST;
@ -551,19 +544,12 @@ static void sequencer_preprocess_transform_crop(
case SEQ_TRANSFORM_FILTER_CUBIC_MITCHELL: case SEQ_TRANSFORM_FILTER_CUBIC_MITCHELL:
filter = IMB_FILTER_CUBIC_MITCHELL; filter = IMB_FILTER_CUBIC_MITCHELL;
break; break;
case SEQ_TRANSFORM_FILTER_NEAREST_3x3: case SEQ_TRANSFORM_FILTER_BOX:
filter = IMB_FILTER_NEAREST; filter = IMB_FILTER_BOX;
num_subsamples = 3;
break; break;
} }
IMB_transform(in, IMB_transform(in, out, IMB_TRANSFORM_MODE_CROP_SRC, filter, transform_matrix, &source_crop);
out,
IMB_TRANSFORM_MODE_CROP_SRC,
filter,
num_subsamples,
transform_matrix,
&source_crop);
if (!seq_image_transform_transparency_gained(context, seq)) { if (!seq_image_transform_transparency_gained(context, seq)) {
out->planes = in->planes; out->planes = in->planes;