Blender V5.0
imbuf/intern/transform.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
2 * SPDX-FileCopyrightText: 2024 Blender Authors
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later */
5
9
10#include <type_traits>
11
12#include "BLI_math_color.h"
13#include "BLI_math_interp.hh"
14#include "BLI_math_matrix.hh"
16#include "BLI_math_vector.h"
17#include "BLI_rect.h"
18#include "BLI_task.hh"
19
20#include "IMB_imbuf.hh"
21#include "IMB_interp.hh"
22
23using blender::float4;
24using blender::uchar4;
25
27
29 const ImBuf *src;
32
33 /* UV coordinates at the destination origin (0,0) in source image space. */
35
36 /* Source UV step delta, when moving along one destination pixel in X axis. */
38
39 /* Source UV step delta, when moving along one destination pixel in Y axis. */
41
42 /* Source corners in destination pixel space, counter-clockwise. */
44
47
48 /* Cropping region in source image pixel space. */
50
51 void init(const float3x3 &transform_matrix, const bool has_source_crop)
52 {
53 start_uv = transform_matrix.location().xy();
54 add_x = transform_matrix.x_axis().xy();
55 add_y = transform_matrix.y_axis().xy();
56 init_destination_region(transform_matrix, has_source_crop);
57 }
58
59 private:
60 void init_destination_region(const float3x3 &transform_matrix, const bool has_source_crop)
61 {
62 if (!has_source_crop) {
65 return;
66 }
67
68 /* Transform the src_crop to the destination buffer with a margin. */
69 const int2 margin(2);
70 rcti rect;
72 float3x3 inverse = math::invert(transform_matrix);
73 const int2 src_coords[4] = {int2(src_crop.xmin, src_crop.ymin),
77 for (int i = 0; i < 4; i++) {
78 int2 src_co = src_coords[i];
79 float2 dst_co = math::transform_point(inverse, float2(src_co));
80 src_corners[i] = float2(dst_co.x, dst_co.y);
81
82 BLI_rcti_do_minmax_v(&rect, int2(dst_co) + margin);
83 BLI_rcti_do_minmax_v(&rect, int2(dst_co) - margin);
84 }
85
86 /* Clamp rect to fit inside the image buffer. */
87 rcti dest_rect;
88 BLI_rcti_init(&dest_rect, 0, dst->x, 0, dst->y);
89 BLI_rcti_isect(&rect, &dest_rect, &rect);
92 }
93};
94
95/* Crop uv-coordinates that are outside the user data src_crop rect. */
96static bool should_discard(const TransformContext &ctx, const float2 &uv)
97{
98 return uv.x < ctx.src_crop.xmin || uv.x >= ctx.src_crop.xmax || uv.y < ctx.src_crop.ymin ||
99 uv.y >= ctx.src_crop.ymax;
100}
101
102template<typename T> static T *init_pixel_pointer(const ImBuf *image, int x, int y);
103template<> uchar *init_pixel_pointer(const ImBuf *image, int x, int y)
104{
105 return image->byte_buffer.data + (size_t(y) * image->x + x) * image->channels;
106}
107template<> float *init_pixel_pointer(const ImBuf *image, int x, int y)
108{
109 return image->float_buffer.data + (size_t(y) * image->x + x) * image->channels;
110}
111
112static float wrap_uv(float value, int size)
113{
114 int x = int(floorf(value));
115 if (UNLIKELY(x < 0 || x >= size)) {
116 x %= size;
117 if (x < 0) {
118 x += size;
119 }
120 }
121 return x;
122}
123
124/* Read a pixel from an image buffer, with filtering/wrapping parameters. */
125template<eIMBInterpolationFilterMode Filter, typename T, int NumChannels, bool WrapUV>
126static void sample_image(const ImBuf *source, float u, float v, T *r_sample)
127{
128 if constexpr (WrapUV) {
129 u = wrap_uv(u, source->x);
130 v = wrap_uv(v, source->y);
131 }
132 /* Bilinear/cubic interpolation functions use `floor(uv)` and `floor(uv)+1`
133 * texels. For proper mapping between pixel and texel spaces, need to
134 * subtract 0.5. */
135 if constexpr (Filter != IMB_FILTER_NEAREST) {
136 u -= 0.5f;
137 v -= 0.5f;
138 }
139 if constexpr (Filter == IMB_FILTER_BILINEAR && std::is_same_v<T, float> && NumChannels == 4) {
140 interpolate_bilinear_fl(source, r_sample, u, v);
141 }
142 else if constexpr (Filter == IMB_FILTER_NEAREST && std::is_same_v<T, uchar> && NumChannels == 4)
143 {
144 interpolate_nearest_border_byte(source, r_sample, u, v);
145 }
146 else if constexpr (Filter == IMB_FILTER_BILINEAR && std::is_same_v<T, uchar> && NumChannels == 4)
147 {
148 interpolate_bilinear_byte(source, r_sample, u, v);
149 }
150 else if constexpr (Filter == IMB_FILTER_BILINEAR && std::is_same_v<T, float>) {
151 if constexpr (WrapUV) {
153 r_sample,
154 source->x,
155 source->y,
156 NumChannels,
157 u,
158 v,
161 }
162 else {
164 source->float_buffer.data, r_sample, source->x, source->y, NumChannels, u, v);
165 }
166 }
167 else if constexpr (Filter == IMB_FILTER_NEAREST && std::is_same_v<T, float>) {
169 source->float_buffer.data, r_sample, source->x, source->y, NumChannels, u, v);
170 }
171 else if constexpr (Filter == IMB_FILTER_CUBIC_BSPLINE && std::is_same_v<T, float>) {
173 source->float_buffer.data, r_sample, source->x, source->y, NumChannels, u, v);
174 }
175 else if constexpr (Filter == IMB_FILTER_CUBIC_BSPLINE && std::is_same_v<T, uchar> &&
176 NumChannels == 4)
177 {
178 interpolate_cubic_bspline_byte(source, r_sample, u, v);
179 }
180 else if constexpr (Filter == IMB_FILTER_CUBIC_MITCHELL && std::is_same_v<T, float>) {
182 source->float_buffer.data, r_sample, source->x, source->y, NumChannels, u, v);
183 }
184 else if constexpr (Filter == IMB_FILTER_CUBIC_MITCHELL && std::is_same_v<T, uchar> &&
185 NumChannels == 4)
186 {
187 interpolate_cubic_mitchell_byte(source, r_sample, u, v);
188 }
189 else {
190 /* Unsupported sampler. */
192 }
193}
194
195static void add_subsample(const float src[4], float dst[4])
196{
197 add_v4_v4(dst, src);
198}
199
200static void add_subsample(const uchar src[4], float dst[4])
201{
202 float premul[4];
204 add_v4_v4(dst, premul);
205}
206
207static void store_premul_float_sample(const float sample[4], float dst[4])
208{
209 copy_v4_v4(dst, sample);
210}
211
212static void store_premul_float_sample(const float sample[4], uchar dst[4])
213{
215}
216
217template<int SrcChannels> static void store_sample(const uchar *sample, uchar *dst)
218{
219 BLI_STATIC_ASSERT(SrcChannels == 4, "Unsigned chars always have 4 channels.");
221}
222
223template<int SrcChannels> static void store_sample(const float *sample, float *dst)
224{
225 if constexpr (SrcChannels == 4) {
226 copy_v4_v4(dst, sample);
227 }
228 else if constexpr (SrcChannels == 3) {
229 copy_v4_fl4(dst, sample[0], sample[1], sample[2], 1.0f);
230 }
231 else if constexpr (SrcChannels == 2) {
232 copy_v4_fl4(dst, sample[0], sample[1], 0.0f, 1.0f);
233 }
234 else if constexpr (SrcChannels == 1) {
235 /* NOTE: single channel sample is stored as grayscale. */
236 copy_v4_fl4(dst, sample[0], sample[0], sample[0], 1.0f);
237 }
238 else {
240 }
241}
242
243/* Process a block of destination image scanlines. */
244template<eIMBInterpolationFilterMode Filter,
245 typename T,
246 int SrcChannels,
247 bool CropSource,
248 bool WrapUV>
249static void process_scanlines(const TransformContext &ctx, IndexRange y_range)
250{
251 if constexpr (Filter == IMB_FILTER_BOX) {
252
253 /* Multiple samples per pixel: accumulate them pre-multiplied,
254 * divide by sample count and write out (un-pre-multiplying if writing out
255 * to byte image).
256 *
257 * Do a box filter: for each destination pixel, accumulate XxY samples from source,
258 * based on scaling factors (length of X/Y pixel steps). Use at least 2 samples
259 * along each direction, so that in case of rotation the image gets
260 * some anti-aliasing. Use at most 100 samples along each direction,
261 * just as some way of clamping possible upper cost. Scaling something down by more
262 * than 100x should rarely if ever happen, worst case they will get some aliasing.
263 */
264 float2 uv_start = ctx.start_uv;
265 int sub_count_x = int(math::clamp(roundf(math::length(ctx.add_x)), 2.0f, 100.0f));
266 int sub_count_y = int(math::clamp(roundf(math::length(ctx.add_y)), 2.0f, 100.0f));
267 const float inv_count = 1.0f / (sub_count_x * sub_count_y);
268 const float2 sub_step_x = ctx.add_x / sub_count_x;
269 const float2 sub_step_y = ctx.add_y / sub_count_y;
270
271 for (int yi : y_range) {
273 float2 uv_row = uv_start + yi * ctx.add_y;
274 for (int xi : ctx.dst_region_x_range) {
275 const float2 uv = uv_row + xi * ctx.add_x;
276 float sample[4] = {};
277
278 for (int sub_y = 0; sub_y < sub_count_y; sub_y++) {
279 for (int sub_x = 0; sub_x < sub_count_x; sub_x++) {
280 float2 delta = (sub_x + 0.5f) * sub_step_x + (sub_y + 0.5f) * sub_step_y;
281 float2 sub_uv = uv + delta;
282 if (!CropSource || !should_discard(ctx, sub_uv)) {
283 T sub_sample[4];
285 T,
286 SrcChannels,
287 WrapUV>(ctx.src, sub_uv.x, sub_uv.y, sub_sample);
288 add_subsample(sub_sample, sample);
289 }
290 }
291 }
292
293 mul_v4_v4fl(sample, sample, inv_count);
295
296 output += 4;
297 }
298 }
299 }
300 else {
301 /* One sample per pixel.
302 * NOTE: sample at pixel center for proper filtering. */
303 float2 uv_start = ctx.start_uv + ctx.add_x * 0.5f + ctx.add_y * 0.5f;
304 for (int yi : y_range) {
306 float2 uv_row = uv_start + yi * ctx.add_y;
307 for (int xi : ctx.dst_region_x_range) {
308 float2 uv = uv_row + xi * ctx.add_x;
309 if (!CropSource || !should_discard(ctx, uv)) {
310 T sample[4];
313 }
314 output += 4;
315 }
316 }
317 }
318}
319
320template<eIMBInterpolationFilterMode Filter, typename T, int SrcChannels>
321static void transform_scanlines(const TransformContext &ctx, IndexRange y_range)
322{
323 switch (ctx.mode) {
326 break;
329 break;
332 break;
333 default:
335 break;
336 }
337}
338
339template<eIMBInterpolationFilterMode Filter>
341{
342 int channels = ctx.src->channels;
343
344 if (ctx.dst->float_buffer.data && ctx.src->float_buffer.data) {
345 /* Float pixels. */
346 if (channels == 4) {
348 }
349 else if (channels == 3) {
351 }
352 else if (channels == 2) {
354 }
355 else if (channels == 1) {
357 }
358 }
359
360 if (ctx.dst->byte_buffer.data && ctx.src->byte_buffer.data) {
361 /* Byte pixels. */
362 if (channels == 4) {
364 }
365 }
366}
367
368static float calc_coverage(float2 pos, int2 ipos, float2 delta, bool is_steep)
369{
370 /* Very approximate: just take difference from coordinate (x or y based on
371 * steepness) to the integer coordinate. Adjust based on directions
372 * of the edges. */
373 float cov;
374 if (is_steep) {
375 cov = fabsf(ipos.x - pos.x);
376 if (delta.y < 0) {
377 cov = 1.0f - cov;
378 }
379 }
380 else {
381 cov = fabsf(ipos.y - pos.y);
382 if (delta.x > 0) {
383 cov = 1.0f - cov;
384 }
385 }
386 cov = math::clamp(cov, 0.0f, 1.0f);
387 /* Resulting coverage is 0.5 .. 1.0 range, since we are only covering
388 * half of the pixels that should be AA'd (the other half is outside the
389 * quad and does not get rasterized). Square the coverage to get
390 * more range, and it looks a bit nicer that way. */
391 cov *= cov;
392 return cov;
393}
394
395static void edge_aa(const TransformContext &ctx)
396{
397 /* Rasterize along outer source edges into the destination image,
398 * reducing alpha based on pixel distance to the edge at each pixel.
399 * This is very approximate and not 100% correct "analytical AA",
400 * but simple to do and better than nothing. */
401 for (int line_idx = 0; line_idx < 4; line_idx++) {
402 float2 ptA = ctx.src_corners[line_idx];
403 float2 ptB = ctx.src_corners[(line_idx + 1) & 3];
404 float2 delta = ptB - ptA;
405 float2 abs_delta = math::abs(delta);
406 float length = math::max(abs_delta.x, abs_delta.y);
407 if (length < 1) {
408 continue;
409 }
410 bool is_steep = length == abs_delta.y;
411
412 /* It is very common to have non-rotated strips; check if edge line is
413 * horizontal or vertical and would not alter the coverage and can
414 * be skipped. */
415 constexpr float NO_ROTATION = 1.0e-6f;
416 constexpr float NO_AA_CONTRIB = 1.0e-2f;
417 if (is_steep) {
418 if ((abs_delta.x < NO_ROTATION) && (fabsf(ptA.x - roundf(ptA.x)) < NO_AA_CONTRIB)) {
419 continue;
420 }
421 }
422 else {
423 if ((abs_delta.y < NO_ROTATION) && (fabsf(ptA.y - roundf(ptA.y)) < NO_AA_CONTRIB)) {
424 continue;
425 }
426 }
427
428 /* DDA line raster: step one pixel along the longer direction. */
429 delta /= length;
430 if (ctx.dst->float_buffer.data != nullptr) {
431 /* Float pixels. */
432 float *dst = ctx.dst->float_buffer.data;
433 for (int i = 0; i < length; i++) {
434 float2 pos = ptA + i * delta;
435 int2 ipos = int2(pos);
436 if (ipos.x >= 0 && ipos.x < ctx.dst->x && ipos.y >= 0 && ipos.y < ctx.dst->y) {
437 float cov = calc_coverage(pos, ipos, delta, is_steep);
438 size_t idx = (size_t(ipos.y) * ctx.dst->x + ipos.x) * 4;
439 dst[idx + 0] *= cov;
440 dst[idx + 1] *= cov;
441 dst[idx + 2] *= cov;
442 dst[idx + 3] *= cov;
443 }
444 }
445 }
446 if (ctx.dst->byte_buffer.data != nullptr) {
447 /* Byte pixels. */
448 uchar *dst = ctx.dst->byte_buffer.data;
449 for (int i = 0; i < length; i++) {
450 float2 pos = ptA + i * delta;
451 int2 ipos = int2(pos);
452 if (ipos.x >= 0 && ipos.x < ctx.dst->x && ipos.y >= 0 && ipos.y < ctx.dst->y) {
453 float cov = calc_coverage(pos, ipos, delta, is_steep);
454 size_t idx = (size_t(ipos.y) * ctx.dst->x + ipos.x) * 4;
455 dst[idx + 3] *= cov;
456 }
457 }
458 }
459 }
460}
461
462} // namespace blender::imbuf::transform
463
464using namespace blender::imbuf::transform;
465using namespace blender;
466
467void IMB_transform(const ImBuf *src,
468 ImBuf *dst,
469 const eIMBTransformMode mode,
471 const float3x3 &transform_matrix,
472 const rctf *src_crop)
473{
474 BLI_assert_msg(mode != IMB_TRANSFORM_MODE_CROP_SRC || src_crop != nullptr,
475 "No source crop rect given, but crop source is requested. Or source crop rect "
476 "was given, but crop source was not requested.");
477 BLI_assert_msg(dst->channels == 4, "Destination image must have 4 channels.");
478
480 ctx.src = src;
481 ctx.dst = dst;
482 ctx.mode = mode;
483 bool crop = mode == IMB_TRANSFORM_MODE_CROP_SRC;
484 if (crop) {
485 ctx.src_crop = *src_crop;
486 }
487 ctx.init(transform_matrix, crop);
488
490 if (filter == IMB_FILTER_NEAREST) {
491 transform_scanlines_filter<IMB_FILTER_NEAREST>(ctx, y_range);
492 }
493 else if (filter == IMB_FILTER_BILINEAR) {
495 }
496 else if (filter == IMB_FILTER_CUBIC_BSPLINE) {
498 }
499 else if (filter == IMB_FILTER_CUBIC_MITCHELL) {
501 }
502 else if (filter == IMB_FILTER_BOX) {
504 }
505 });
506
507 if (crop && (filter != IMB_FILTER_NEAREST)) {
508 edge_aa(ctx);
509 }
510}
#define BLI_assert_unreachable()
Definition BLI_assert.h:93
#define BLI_STATIC_ASSERT(a, msg)
Definition BLI_assert.h:83
#define BLI_assert_msg(a, msg)
Definition BLI_assert.h:53
MINLINE void straight_uchar_to_premul_float(float result[4], const unsigned char color[4])
MINLINE void premul_float_to_straight_uchar(unsigned char *result, const float color[4])
MINLINE void add_v4_v4(float r[4], const float a[4])
MINLINE void copy_v4_v4(float r[4], const float a[4])
MINLINE void copy_v4_fl4(float v[4], float x, float y, float z, float w)
MINLINE void copy_v4_v4_uchar(unsigned char r[4], const unsigned char a[4])
MINLINE void mul_v4_v4fl(float r[4], const float a[4], float f)
BLI_INLINE int BLI_rcti_size_y(const struct rcti *rct)
Definition BLI_rect.h:198
void BLI_rcti_init_minmax(struct rcti *rect)
Definition rct.cc:474
void BLI_rcti_init(struct rcti *rect, int xmin, int xmax, int ymin, int ymax)
Definition rct.cc:414
bool BLI_rcti_isect(const struct rcti *src1, const struct rcti *src2, struct rcti *dest)
BLI_INLINE int BLI_rcti_size_x(const struct rcti *rct)
Definition BLI_rect.h:194
void BLI_rcti_do_minmax_v(struct rcti *rect, const int xy[2])
Definition rct.cc:486
unsigned char uchar
#define UNLIKELY(x)
eIMBTransformMode
Transform modes to use for IMB_transform function.
Definition IMB_imbuf.hh:526
@ IMB_TRANSFORM_MODE_WRAP_REPEAT
Wrap repeat the source buffer. Only supported in with nearest filtering.
Definition IMB_imbuf.hh:532
@ IMB_TRANSFORM_MODE_REGULAR
Do not crop or repeat.
Definition IMB_imbuf.hh:528
@ IMB_TRANSFORM_MODE_CROP_SRC
Crop the source buffer.
Definition IMB_imbuf.hh:530
eIMBInterpolationFilterMode
Definition IMB_imbuf.hh:280
@ IMB_FILTER_NEAREST
Definition IMB_imbuf.hh:281
@ IMB_FILTER_CUBIC_BSPLINE
Definition IMB_imbuf.hh:283
@ IMB_FILTER_CUBIC_MITCHELL
Definition IMB_imbuf.hh:284
@ IMB_FILTER_BILINEAR
Definition IMB_imbuf.hh:282
@ IMB_FILTER_BOX
Definition IMB_imbuf.hh:285
ATTR_WARN_UNUSED_RESULT const BMVert * v
static DBVT_INLINE btScalar size(const btDbvtVolume &a)
Definition btDbvt.cpp:52
constexpr int64_t first() const
#define roundf(x)
uint pos
#define filter
#define output
MatBase< C, R > inverse(MatBase< C, R >) RET
float length(VecOp< float, D >) RET
void IMB_transform(const ImBuf *src, ImBuf *dst, const eIMBTransformMode mode, const eIMBInterpolationFilterMode filter, const float3x3 &transform_matrix, const rctf *src_crop)
Transform source image buffer onto destination image buffer using a transform matrix.
static void store_sample(const uchar *sample, uchar *dst)
static void sample_image(const ImBuf *source, float u, float v, T *r_sample)
static void add_subsample(const float src[4], float dst[4])
static void edge_aa(const TransformContext &ctx)
static float calc_coverage(float2 pos, int2 ipos, float2 delta, bool is_steep)
static T * init_pixel_pointer(const ImBuf *image, int x, int y)
static void transform_scanlines_filter(const TransformContext &ctx, IndexRange y_range)
static void store_premul_float_sample(const float sample[4], float dst[4])
static void process_scanlines(const TransformContext &ctx, IndexRange y_range)
static bool should_discard(const TransformContext &ctx, const float2 &uv)
static float wrap_uv(float value, int size)
static void transform_scanlines(const TransformContext &ctx, IndexRange y_range)
float4 interpolate_bilinear_fl(const ImBuf *in, float u, float v)
Definition IMB_interp.hh:57
uchar4 interpolate_nearest_border_byte(const ImBuf *in, float u, float v)
Definition IMB_interp.hh:23
uchar4 interpolate_bilinear_byte(const ImBuf *in, float u, float v)
Definition IMB_interp.hh:53
uchar4 interpolate_cubic_mitchell_byte(const ImBuf *in, float u, float v)
uchar4 interpolate_cubic_bspline_byte(const ImBuf *in, float u, float v)
T clamp(const T &a, const T &min, const T &max)
void interpolate_nearest_border_fl(const float *buffer, float *output, int width, int height, int components, float u, float v)
T length(const VecBase< T, Size > &a)
float4 interpolate_cubic_bspline_fl(const float *buffer, int width, int height, float u, float v)
CartesianBasis invert(const CartesianBasis &basis)
float4 interpolate_bilinear_fl(const float *buffer, int width, int height, float u, float v)
float4 interpolate_cubic_mitchell_fl(const float *buffer, int width, int height, float u, float v)
T max(const T &a, const T &b)
T abs(const T &a)
void interpolate_bilinear_wrapmode_fl(const float *buffer, float *output, int width, int height, int components, float u, float v, InterpWrapMode wrap_u, InterpWrapMode wrap_v)
VecBase< T, 3 > transform_point(const CartesianBasis &basis, const VecBase< T, 3 > &v)
void parallel_for(const IndexRange range, const int64_t grain_size, const Function &function, const TaskSizeHints &size_hints=detail::TaskSizeHints_Static(1))
Definition BLI_task.hh:93
blender::VecBase< uint8_t, 4 > uchar4
VecBase< float, 4 > float4
VecBase< int32_t, 2 > int2
VecBase< float, 2 > float2
MatBase< float, 3, 3 > float3x3
#define floorf
#define fabsf
ImBufFloatBuffer float_buffer
ImBufByteBuffer byte_buffer
VecBase< T, 2 > xy() const
void init(const float3x3 &transform_matrix, const bool has_source_crop)
float xmax
float xmin
float ymax
float ymin
int ymin
int xmin
i
Definition text_draw.cc:230