Files
test/source/blender/imbuf/intern/transform.cc
Campbell Barton 7f7648c6ed Cleanup: spelling in code comments & minor edits
- Use uppercase NOTE: tags.
- Correct bNote -> bNode.
- Use colon after parameters.
- Use doxy-style doc-strings.
2024-06-06 09:55:13 +10:00

510 lines
17 KiB
C++

/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup imbuf
*/
#include <type_traits>
#include "BLI_math_color_blend.h"
#include "BLI_math_interp.hh"
#include "BLI_math_matrix.hh"
#include "BLI_math_vector.h"
#include "BLI_rect.h"
#include "BLI_task.hh"
#include "IMB_imbuf.hh"
#include "IMB_interp.hh"
using blender::float4;
using blender::uchar4;
namespace blender::imbuf::transform {
struct TransformContext {
const ImBuf *src;
ImBuf *dst;
eIMBTransformMode mode;
/* UV coordinates at the destination origin (0,0) in source image space. */
float2 start_uv;
/* Source UV step delta, when moving along one destination pixel in X axis. */
float2 add_x;
/* Source UV step delta, when moving along one destination pixel in Y axis. */
float2 add_y;
/* Source corners in destination pixel space, counter-clockwise. */
float2 src_corners[4];
IndexRange dst_region_x_range;
IndexRange dst_region_y_range;
/* Cropping region in source image pixel space. */
rctf src_crop;
void init(const float4x4 &transform_matrix, const bool has_source_crop)
{
start_uv = transform_matrix.location().xy();
add_x = transform_matrix.x_axis().xy();
add_y = transform_matrix.y_axis().xy();
init_destination_region(transform_matrix, has_source_crop);
}
private:
void init_destination_region(const float4x4 &transform_matrix, const bool has_source_crop)
{
if (!has_source_crop) {
dst_region_x_range = IndexRange(dst->x);
dst_region_y_range = IndexRange(dst->y);
return;
}
/* Transform the src_crop to the destination buffer with a margin. */
const int2 margin(2);
rcti rect;
BLI_rcti_init_minmax(&rect);
float4x4 inverse = math::invert(transform_matrix);
const int2 src_coords[4] = {int2(src_crop.xmin, src_crop.ymin),
int2(src_crop.xmax, src_crop.ymin),
int2(src_crop.xmax, src_crop.ymax),
int2(src_crop.xmin, src_crop.ymax)};
for (int i = 0; i < 4; i++) {
int2 src_co = src_coords[i];
float3 dst_co = math::transform_point(inverse, float3(src_co.x, src_co.y, 0.0f));
src_corners[i] = float2(dst_co.x, dst_co.y);
BLI_rcti_do_minmax_v(&rect, int2(dst_co) + margin);
BLI_rcti_do_minmax_v(&rect, int2(dst_co) - margin);
}
/* Clamp rect to fit inside the image buffer. */
rcti dest_rect;
BLI_rcti_init(&dest_rect, 0, dst->x, 0, dst->y);
BLI_rcti_isect(&rect, &dest_rect, &rect);
dst_region_x_range = IndexRange(rect.xmin, BLI_rcti_size_x(&rect));
dst_region_y_range = IndexRange(rect.ymin, BLI_rcti_size_y(&rect));
}
};
/* Crop uv-coordinates that are outside the user data src_crop rect. */
static bool should_discard(const TransformContext &ctx, const float2 &uv)
{
return uv.x < ctx.src_crop.xmin || uv.x >= ctx.src_crop.xmax || uv.y < ctx.src_crop.ymin ||
uv.y >= ctx.src_crop.ymax;
}
template<typename T> static T *init_pixel_pointer(const ImBuf *image, int x, int y);
template<> uchar *init_pixel_pointer(const ImBuf *image, int x, int y)
{
return image->byte_buffer.data + (size_t(y) * image->x + x) * image->channels;
}
template<> float *init_pixel_pointer(const ImBuf *image, int x, int y)
{
return image->float_buffer.data + (size_t(y) * image->x + x) * image->channels;
}
static float wrap_uv(float value, int size)
{
int x = int(floorf(value));
if (UNLIKELY(x < 0 || x >= size)) {
x %= size;
if (x < 0) {
x += size;
}
}
return x;
}
/* Read a pixel from an image buffer, with filtering/wrapping parameters. */
template<eIMBInterpolationFilterMode Filter, typename T, int NumChannels, bool WrapUV>
static void sample_image(const ImBuf *source, float u, float v, T *r_sample)
{
if constexpr (WrapUV) {
u = wrap_uv(u, source->x);
v = wrap_uv(v, source->y);
}
/* 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. */
if constexpr (Filter != IMB_FILTER_NEAREST) {
u -= 0.5f;
v -= 0.5f;
}
if constexpr (Filter == IMB_FILTER_BILINEAR && std::is_same_v<T, float> && NumChannels == 4) {
interpolate_bilinear_fl(source, r_sample, u, v);
}
else if constexpr (Filter == IMB_FILTER_NEAREST && std::is_same_v<T, uchar> && NumChannels == 4)
{
interpolate_nearest_border_byte(source, r_sample, u, v);
}
else if constexpr (Filter == IMB_FILTER_BILINEAR && std::is_same_v<T, uchar> && NumChannels == 4)
{
interpolate_bilinear_byte(source, r_sample, u, v);
}
else if constexpr (Filter == IMB_FILTER_BILINEAR && std::is_same_v<T, float>) {
if constexpr (WrapUV) {
math::interpolate_bilinear_wrap_fl(source->float_buffer.data,
r_sample,
source->x,
source->y,
NumChannels,
u,
v,
true,
true);
}
else {
math::interpolate_bilinear_fl(
source->float_buffer.data, r_sample, source->x, source->y, NumChannels, u, v);
}
}
else if constexpr (Filter == IMB_FILTER_NEAREST && std::is_same_v<T, float>) {
math::interpolate_nearest_border_fl(
source->float_buffer.data, r_sample, source->x, source->y, NumChannels, u, v);
}
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_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();
}
}
static void add_subsample(const float src[4], float dst[4])
{
add_v4_v4(dst, src);
}
static void add_subsample(const uchar src[4], float dst[4])
{
float premul[4];
straight_uchar_to_premul_float(premul, src);
add_v4_v4(dst, premul);
}
static void store_premul_float_sample(const float sample[4], float dst[4])
{
copy_v4_v4(dst, sample);
}
static void store_premul_float_sample(const float sample[4], uchar dst[4])
{
premul_float_to_straight_uchar(dst, sample);
}
template<int SrcChannels> static void store_sample(const uchar *sample, uchar *dst)
{
BLI_STATIC_ASSERT(SrcChannels == 4, "Unsigned chars always have 4 channels.");
copy_v4_v4_uchar(dst, sample);
}
template<int SrcChannels> static void store_sample(const float *sample, float *dst)
{
if constexpr (SrcChannels == 4) {
copy_v4_v4(dst, sample);
}
else if constexpr (SrcChannels == 3) {
copy_v4_fl4(dst, sample[0], sample[1], sample[2], 1.0f);
}
else if constexpr (SrcChannels == 2) {
copy_v4_fl4(dst, sample[0], sample[1], 0.0f, 1.0f);
}
else if constexpr (SrcChannels == 1) {
/* NOTE: single channel sample is stored as grayscale. */
copy_v4_fl4(dst, sample[0], sample[0], sample[0], 1.0f);
}
else {
BLI_assert_unreachable();
}
}
/* Process a block of destination image scanlines. */
template<eIMBInterpolationFilterMode Filter,
typename T,
int SrcChannels,
bool CropSource,
bool WrapUV>
static void process_scanlines(const TransformContext &ctx, IndexRange y_range)
{
if constexpr (Filter == IMB_FILTER_BOX) {
/* Multiple samples per pixel: accumulate them pre-multiplied,
* divide by sample count and write out (un-pre-multiplying if writing out
* to byte image).
*
* 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 image gets
* some anti-aliasing. 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) {
T *output = init_pixel_pointer<T>(ctx.dst, ctx.dst_region_x_range.first(), yi);
float2 uv_row = uv_start + yi * ctx.add_y;
for (int xi : ctx.dst_region_x_range) {
const float2 uv = uv_row + xi * ctx.add_x;
float sample[4] = {};
for (int sub_y = 0; sub_y < sub_count_y; sub_y++) {
for (int sub_x = 0; sub_x < sub_count_x; sub_x++) {
float2 delta = (sub_x + 0.5f) * sub_step_x + (sub_y + 0.5f) * sub_step_y;
float2 sub_uv = uv + delta;
if (!CropSource || !should_discard(ctx, sub_uv)) {
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);
}
}
}
mul_v4_v4fl(sample, sample, inv_count);
store_premul_float_sample(sample, output);
output += 4;
}
}
}
else {
/* 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) {
T *output = init_pixel_pointer<T>(ctx.dst, ctx.dst_region_x_range.first(), yi);
float2 uv_row = uv_start + yi * ctx.add_y;
for (int xi : ctx.dst_region_x_range) {
float2 uv = uv_row + xi * ctx.add_x;
if (!CropSource || !should_discard(ctx, uv)) {
T sample[4];
sample_image<Filter, T, SrcChannels, WrapUV>(ctx.src, uv.x, uv.y, sample);
store_sample<SrcChannels>(sample, output);
}
output += 4;
}
}
}
}
template<eIMBInterpolationFilterMode Filter, typename T, int SrcChannels>
static void transform_scanlines(const TransformContext &ctx, IndexRange y_range)
{
switch (ctx.mode) {
case IMB_TRANSFORM_MODE_REGULAR:
process_scanlines<Filter, T, SrcChannels, false, false>(ctx, y_range);
break;
case IMB_TRANSFORM_MODE_CROP_SRC:
process_scanlines<Filter, T, SrcChannels, true, false>(ctx, y_range);
break;
case IMB_TRANSFORM_MODE_WRAP_REPEAT:
process_scanlines<Filter, T, SrcChannels, false, true>(ctx, y_range);
break;
default:
BLI_assert_unreachable();
break;
}
}
template<eIMBInterpolationFilterMode Filter>
static void transform_scanlines_filter(const TransformContext &ctx, IndexRange y_range)
{
int channels = ctx.src->channels;
if (ctx.dst->float_buffer.data && ctx.src->float_buffer.data) {
/* Float pixels. */
if (channels == 4) {
transform_scanlines<Filter, float, 4>(ctx, y_range);
}
else if (channels == 3) {
transform_scanlines<Filter, float, 3>(ctx, y_range);
}
else if (channels == 2) {
transform_scanlines<Filter, float, 2>(ctx, y_range);
}
else if (channels == 1) {
transform_scanlines<Filter, float, 1>(ctx, y_range);
}
}
if (ctx.dst->byte_buffer.data && ctx.src->byte_buffer.data) {
/* Byte pixels. */
if (channels == 4) {
transform_scanlines<Filter, uchar, 4>(ctx, y_range);
}
}
}
static float calc_coverage(float2 pos, int2 ipos, float2 delta, bool is_steep)
{
/* Very approximate: just take difference from coordinate (x or y based on
* steepness) to the integer coordinate. Adjust based on directions
* of the edges. */
float cov;
if (is_steep) {
cov = fabsf(ipos.x - pos.x);
if (delta.y < 0) {
cov = 1.0f - cov;
}
}
else {
cov = fabsf(ipos.y - pos.y);
if (delta.x > 0) {
cov = 1.0f - cov;
}
}
cov = math::clamp(cov, 0.0f, 1.0f);
/* Resulting coverage is 0.5 .. 1.0 range, since we are only covering
* half of the pixels that should be AA'd (the other half is outside the
* quad and does not get rasterized). Square the coverage to get
* more range, and it looks a bit nicer that way. */
cov *= cov;
return cov;
}
static void edge_aa(const TransformContext &ctx)
{
/* Rasterize along outer source edges into the destination image,
* reducing alpha based on pixel distance to the edge at each pixel.
* This is very approximate and not 100% correct "analytical AA",
* but simple to do and better than nothing. */
for (int line_idx = 0; line_idx < 4; line_idx++) {
float2 ptA = ctx.src_corners[line_idx];
float2 ptB = ctx.src_corners[(line_idx + 1) & 3];
float2 delta = ptB - ptA;
float2 abs_delta = math::abs(delta);
float length = math::max(abs_delta.x, abs_delta.y);
if (length < 1) {
continue;
}
bool is_steep = length == abs_delta.y;
/* It is very common to have non-rotated strips; check if edge line is
* horizontal or vertical and would not alter the coverage and can
* be skipped. */
constexpr float NO_ROTATION = 1.0e-6f;
constexpr float NO_AA_CONTRIB = 1.0e-2f;
if (is_steep) {
if ((abs_delta.x < NO_ROTATION) && (fabsf(ptA.x - roundf(ptA.x)) < NO_AA_CONTRIB)) {
continue;
}
}
else {
if ((abs_delta.y < NO_ROTATION) && (fabsf(ptA.y - roundf(ptA.y)) < NO_AA_CONTRIB)) {
continue;
}
}
/* DDA line raster: step one pixel along the longer direction. */
delta /= length;
if (ctx.dst->float_buffer.data != nullptr) {
/* Float pixels. */
float *dst = ctx.dst->float_buffer.data;
for (int i = 0; i < length; i++) {
float2 pos = ptA + i * delta;
int2 ipos = int2(pos);
if (ipos.x >= 0 && ipos.x < ctx.dst->x && ipos.y >= 0 && ipos.y < ctx.dst->y) {
float cov = calc_coverage(pos, ipos, delta, is_steep);
size_t idx = (size_t(ipos.y) * ctx.dst->x + ipos.x) * 4;
dst[idx + 0] *= cov;
dst[idx + 1] *= cov;
dst[idx + 2] *= cov;
dst[idx + 3] *= cov;
}
}
}
if (ctx.dst->byte_buffer.data != nullptr) {
/* Byte pixels. */
uchar *dst = ctx.dst->byte_buffer.data;
for (int i = 0; i < length; i++) {
float2 pos = ptA + i * delta;
int2 ipos = int2(pos);
if (ipos.x >= 0 && ipos.x < ctx.dst->x && ipos.y >= 0 && ipos.y < ctx.dst->y) {
float cov = calc_coverage(pos, ipos, delta, is_steep);
size_t idx = (size_t(ipos.y) * ctx.dst->x + ipos.x) * 4;
dst[idx + 3] *= cov;
}
}
}
}
}
} // namespace blender::imbuf::transform
using namespace blender::imbuf::transform;
using namespace blender;
void IMB_transform(const ImBuf *src,
ImBuf *dst,
const eIMBTransformMode mode,
const eIMBInterpolationFilterMode filter,
const float transform_matrix[4][4],
const rctf *src_crop)
{
BLI_assert_msg(mode != IMB_TRANSFORM_MODE_CROP_SRC || src_crop != nullptr,
"No source crop rect given, but crop source is requested. Or source crop rect "
"was given, but crop source was not requested.");
BLI_assert_msg(dst->channels == 4, "Destination image must have 4 channels.");
TransformContext ctx;
ctx.src = src;
ctx.dst = dst;
ctx.mode = mode;
bool crop = mode == IMB_TRANSFORM_MODE_CROP_SRC;
if (crop) {
ctx.src_crop = *src_crop;
}
ctx.init(blender::float4x4(transform_matrix), crop);
threading::parallel_for(ctx.dst_region_y_range, 8, [&](IndexRange y_range) {
if (filter == IMB_FILTER_NEAREST) {
transform_scanlines_filter<IMB_FILTER_NEAREST>(ctx, y_range);
}
else if (filter == IMB_FILTER_BILINEAR) {
transform_scanlines_filter<IMB_FILTER_BILINEAR>(ctx, y_range);
}
else if (filter == IMB_FILTER_CUBIC_BSPLINE) {
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);
}
else if (filter == IMB_FILTER_BOX) {
transform_scanlines_filter<IMB_FILTER_BOX>(ctx, y_range);
}
});
if (crop && (filter != IMB_FILTER_NEAREST)) {
edge_aa(ctx);
}
}