/* 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 "BLI_math_interp.hh" #include "BLI_math_vector.hh" #include "BLI_task.hh" #include "BLI_utildefines.h" #include "MEM_guardedalloc.h" #include "IMB_filter.hh" #include "IMB_imbuf.hh" #include "IMB_imbuf_types.hh" #include "IMB_metadata.hh" #include "BLI_sys_types.h" /* for intptr_t support */ namespace blender { static inline float4 load_pixel(const uchar4 *ptr) { return float4(ptr[0]); } static inline float4 load_pixel(const float *ptr) { return float4(ptr[0]); } static inline float4 load_pixel(const float2 *ptr) { return float4(ptr[0], 0.0f, 1.0f); } static inline float4 load_pixel(const float3 *ptr) { return float4(ptr[0], 1.0f); } static inline float4 load_pixel(const float4 *ptr) { return float4(ptr[0]); } static inline void store_pixel(float4 pix, uchar4 *ptr) { *ptr = uchar4(math::round(pix)); } static inline void store_pixel(float4 pix, float *ptr) { *ptr = pix.x; } static inline void store_pixel(float4 pix, float2 *ptr) { memcpy(reinterpret_cast(ptr), &pix, sizeof(*ptr)); } static inline void store_pixel(float4 pix, float3 *ptr) { memcpy(reinterpret_cast(ptr), &pix, sizeof(*ptr)); } static inline void store_pixel(float4 pix, float4 *ptr) { *ptr = pix; } template static void to_static_pixel_type(const BufferT *src_buffer, const int channels, BufferT *dst_buffer, const Fn &fn) { if constexpr (std::is_same_v) { fn(reinterpret_cast(src_buffer), reinterpret_cast(dst_buffer)); } else { if (channels == 1) { fn(src_buffer, dst_buffer); } else if (channels == 2) { const float2 *src = reinterpret_cast(src_buffer); fn(src, reinterpret_cast(dst_buffer)); } else if (channels == 3) { const float3 *src = reinterpret_cast(src_buffer); fn(src, reinterpret_cast(dst_buffer)); } else if (channels == 4) { const float4 *src = reinterpret_cast(src_buffer); fn(src, reinterpret_cast(dst_buffer)); } } } template static void scale_down_x_func(const BufferT *src_buffer, const int2 src_size, const int channels, BufferT *dst_buffer, const int2 dst_size, bool threaded) { const int newx = dst_size.x; const int ibufx = src_size.x; const int ibufy = src_size.y; to_static_pixel_type(src_buffer, channels, dst_buffer, [&](const T *src, T *dst) { const float add = (ibufx - 0.01f) / newx; const float inv_add = 1.0f / add; const int grain_size = threaded ? 32 : ibufy; threading::parallel_for(IndexRange(ibufy), grain_size, [&](IndexRange range) { for (const int y : range) { const T *src_ptr = src + (int64_t(y) * ibufx); T *dst_ptr = dst + (int64_t(y) * newx); float sample = 0.0f; float4 val(0.0f); for (int x = 0; x < newx; x++) { float4 nval = -val * sample; sample += add; while (sample >= 1.0f) { sample -= 1.0f; nval += load_pixel(src_ptr); src_ptr++; } val = load_pixel(src_ptr); src_ptr++; float4 pix = (nval + sample * val) * inv_add; store_pixel(pix, dst_ptr); dst_ptr++; sample -= 1.0f; } } }); }); } template static void scale_down_y_func(const BufferT *src_buffer, const int2 src_size, const int channels, BufferT *dst_buffer, const int2 dst_size, bool threaded) { const int newy = dst_size.y; const int ibufx = src_size.x; const int ibufy = src_size.y; to_static_pixel_type(src_buffer, channels, dst_buffer, [&](const T *src, T *dst) { const float add = (ibufy - 0.01f) / newy; const float inv_add = 1.0f / add; const int grain_size = threaded ? 32 : ibufx; threading::parallel_for(IndexRange(ibufx), grain_size, [&](IndexRange range) { for (const int x : range) { const T *src_ptr = src + x; T *dst_ptr = dst + x; float sample = 0.0f; float4 val(0.0f); for (int y = 0; y < newy; y++) { float4 nval = -val * sample; sample += add; while (sample >= 1.0f) { sample -= 1.0f; nval += load_pixel(src_ptr); src_ptr += ibufx; } val = load_pixel(src_ptr); src_ptr += ibufx; float4 pix = (nval + sample * val) * inv_add; store_pixel(pix, dst_ptr); dst_ptr += ibufx; sample -= 1.0f; } } }); }); } template static void scale_up_x_func(const BufferT *src_buffer, const int2 src_size, const int channels, BufferT *dst_buffer, const int2 dst_size, bool threaded) { const int newx = dst_size.x; const int ibufx = src_size.x; const int ibufy = src_size.y; to_static_pixel_type(src_buffer, channels, dst_buffer, [&](const T *src, T *dst) { const float add = (ibufx - 0.001f) / newx; /* Special case: source is 1px wide (see #70356). */ if (UNLIKELY(ibufx == 1)) { for (int y = ibufy; y > 0; y--) { for (int x = newx; x > 0; x--) { *dst = *src; dst++; } src++; } } else { const int grain_size = threaded ? 32 : ibufy; threading::parallel_for(IndexRange(ibufy), grain_size, [&](IndexRange range) { for (const int y : range) { float sample = -0.5f + add * 0.5f; int counter = 0; const T *src_ptr = src + (int64_t(y) * ibufx); T *dst_ptr = dst + (int64_t(y) * newx); float4 val = load_pixel(src_ptr); float4 nval = load_pixel(src_ptr + 1); float4 diff = nval - val; if (ibufx > 2) { src_ptr += 2; counter += 2; } for (int x = 0; x < newx; x++) { if (sample >= 1.0f) { sample -= 1.0f; val = nval; nval = load_pixel(src_ptr); diff = nval - val; if (counter + 1 < ibufx) { src_ptr++; counter++; } } float4 pix = val + math::max(sample, 0.0f) * diff; store_pixel(pix, dst_ptr); dst_ptr++; sample += add; } } }); } }); } template static void scale_up_y_func(const BufferT *src_buffer, const int2 src_size, const int channels, BufferT *dst_buffer, const int2 dst_size, bool threaded) { const int newy = dst_size.y; const int ibufx = src_size.x; const int ibufy = src_size.y; to_static_pixel_type(src_buffer, channels, dst_buffer, [&](const T *src, T *dst) { const float add = (ibufy - 0.001f) / newy; /* Special case: source is 1px high (see #70356). */ if (UNLIKELY(ibufy == 1)) { for (int y = newy; y > 0; y--) { memcpy(reinterpret_cast(dst), src, sizeof(T) * ibufx); dst += ibufx; } } else { const int grain_size = threaded ? 32 : ibufx; threading::parallel_for(IndexRange(ibufx), grain_size, [&](IndexRange range) { for (const int x : range) { float sample = -0.5f + add * 0.5f; int counter = 0; const T *src_ptr = src + x; T *dst_ptr = dst + x; float4 val = load_pixel(src_ptr); float4 nval = load_pixel(src_ptr + ibufx); float4 diff = nval - val; if (ibufy > 2) { src_ptr += ibufx * 2; counter += 2; } for (int y = 0; y < newy; y++) { if (sample >= 1.0f) { sample -= 1.0f; val = nval; nval = load_pixel(src_ptr); diff = nval - val; if (counter + 1 < ibufy) { src_ptr += ibufx; ++counter; } } float4 pix = val + math::max(sample, 0.0f) * diff; store_pixel(pix, dst_ptr); dst_ptr += ibufx; sample += add; } } }); } }); } template static void imb_scale_box(const BufferT *src_buffer, const int2 src_size, const int channels, BufferT *dst_buffer, const int2 dst_size, const bool threaded) { BufferT *tmp_buffer = MEM_new_array_uninitialized( int64_t(channels) * dst_size.x * src_size.y, __func__); if (dst_size.x < src_size.x) { scale_down_x_func( src_buffer, src_size, channels, tmp_buffer, int2(dst_size.x, src_size.y), threaded); } else { scale_up_x_func( src_buffer, src_size, channels, tmp_buffer, int2(dst_size.x, src_size.y), threaded); } if (dst_size.y < src_size.y) { scale_down_y_func( tmp_buffer, int2(dst_size.x, src_size.y), channels, dst_buffer, dst_size, threaded); } else { scale_up_y_func( tmp_buffer, int2(dst_size.x, src_size.y), channels, dst_buffer, dst_size, threaded); } MEM_delete(tmp_buffer); } void IMB_scale_box(const float *src_buffer, const int2 src_size, const int channels, float *dst_buffer, const int2 dst_size, const bool threaded) { imb_scale_box(src_buffer, src_size, channels, dst_buffer, dst_size, threaded); } void IMB_scale_box(const uchar *src_buffer, const int2 src_size, const int channels, uchar *dst_buffer, const int2 dst_size, const bool threaded) { imb_scale_box(src_buffer, src_size, channels, dst_buffer, dst_size, threaded); } template static void scale_nearest( const T *src, T *dst, const int2 src_size, const int2 dst_size, IndexRange y_range) { const int ibufx = src_size.x; const int ibufy = src_size.y; const int newx = dst_size.x; const int newy = dst_size.y; /* Nearest sample scaling. Step through pixels in fixed point coordinates. */ constexpr int FRAC_BITS = 16; int64_t stepx = ((int64_t(ibufx) << FRAC_BITS) + newx / 2) / newx; int64_t stepy = ((int64_t(ibufy) << FRAC_BITS) + newy / 2) / newy; int64_t posy = y_range.first() * stepy; dst += y_range.first() * newx; for (const int y : y_range) { UNUSED_VARS(y); const T *row = src + (posy >> FRAC_BITS) * ibufx; int64_t posx = 0; for (int x = 0; x < newx; x++, posx += stepx) { *dst = row[posx >> FRAC_BITS]; dst++; } posy += stepy; } } template static void scale_nearest_func(const BufferT *src_buffer, const int2 src_size, const int channels, BufferT *dst_buffer, const int2 dst_size, bool threaded) { const int grain_size = threaded ? 64 : dst_size.y; threading::parallel_for(IndexRange(dst_size.y), grain_size, [&](IndexRange y_range) { if constexpr (std::is_same_v) { const uchar4 *src = reinterpret_cast(src_buffer); scale_nearest(src, reinterpret_cast(dst_buffer), src_size, dst_size, y_range); } else { if (channels == 1) { scale_nearest(src_buffer, dst_buffer, src_size, dst_size, y_range); } else if (channels == 2) { const float2 *src = reinterpret_cast(src_buffer); scale_nearest(src, reinterpret_cast(dst_buffer), src_size, dst_size, y_range); } else if (channels == 3) { const float3 *src = reinterpret_cast(src_buffer); scale_nearest(src, reinterpret_cast(dst_buffer), src_size, dst_size, y_range); } else if (channels == 4) { const float4 *src = reinterpret_cast(src_buffer); scale_nearest(src, reinterpret_cast(dst_buffer), src_size, dst_size, y_range); } } }); } template static void scale_bilinear(const BufferT *src_buffer, const int2 src_size, const int channels, BufferT *dst_buffer, const int2 dst_size, bool threaded) { const int newx = dst_size.x; const int newy = dst_size.y; const int grain_size = threaded ? 32 : newy; threading::parallel_for(IndexRange(newy), grain_size, [&](IndexRange y_range) { float factor_x = float(src_size.x) / newx; float factor_y = float(src_size.y) / newy; for (const int y : y_range) { float v = (float(y) + 0.5f) * factor_y - 0.5f; for (int x = 0; x < newx; x++) { float u = (float(x) + 0.5f) * factor_x - 0.5f; int64_t offset = int64_t(y) * newx + x; if constexpr (std::is_same_v) { *reinterpret_cast(dst_buffer + offset * 4) = math::interpolate_bilinear_byte( src_buffer, src_size.x, src_size.y, u, v); } else { float *pixel = dst_buffer + channels * offset; math::interpolate_bilinear_fl(src_buffer, pixel, src_size.x, src_size.y, channels, u, v); } } } }); } bool IMB_scale(ImBuf *ibuf, const int2 new_size, IMBScaleFilter filter, bool threaded) { BLI_assert_msg(new_size.x > 0 && new_size.y > 0, "Images must be at least 1 on both dimensions!"); if (ibuf == nullptr) { return false; } const int2 src_size = int2(ibuf->x, ibuf->y); if (src_size == new_size) { return false; } const ColorSpace *float_colorspace = ibuf->float_buffer.colorspace; const ColorSpace *byte_colorspace = ibuf->byte_buffer.colorspace; switch (filter) { case IMBScaleFilter::Nearest: { if (const float *src = ibuf->float_data()) { float *dst = MEM_new_array_uninitialized( size_t(ibuf->channels) * new_size.x * new_size.y, __func__); scale_nearest_func(src, src_size, ibuf->channels, dst, new_size, threaded); ibuf->assign_float_data(dst); } if (const uchar *src = ibuf->byte_data()) { uchar *dst = MEM_new_array_uninitialized(size_t(new_size.x) * new_size.y * 4, __func__); scale_nearest_func(src, src_size, 4, dst, new_size, threaded); ibuf->assign_byte_data(dst); } break; } case IMBScaleFilter::Bilinear: { if (const float *src = ibuf->float_data()) { float *dst = MEM_new_array_uninitialized( size_t(ibuf->channels) * new_size.x * new_size.y, __func__); scale_bilinear(src, src_size, ibuf->channels, dst, new_size, threaded); ibuf->assign_float_data(dst); } if (const uchar *src = ibuf->byte_data()) { uchar *dst = MEM_new_array_uninitialized(size_t(new_size.x) * new_size.y * 4, __func__); scale_bilinear(src, src_size, 4, dst, new_size, threaded); ibuf->assign_byte_data(dst); } break; } case IMBScaleFilter::Box: { if (const float *src = ibuf->float_data()) { float *dst = MEM_new_array_uninitialized( size_t(ibuf->channels) * new_size.x * new_size.y, __func__); imb_scale_box(src, src_size, ibuf->channels, dst, new_size, threaded); ibuf->assign_float_data(dst); } if (const uchar *src = ibuf->byte_data()) { uchar *dst = MEM_new_array_uninitialized(size_t(new_size.x) * new_size.y * 4, __func__); imb_scale_box(src, src_size, 4, dst, new_size, threaded); ibuf->assign_byte_data(dst); } break; } } ibuf->float_buffer.colorspace = float_colorspace; ibuf->byte_buffer.colorspace = byte_colorspace; ibuf->x = new_size.x; ibuf->y = new_size.y; return true; } ImBuf *IMB_scale_into_new(const ImBuf *ibuf, const int2 new_size, IMBScaleFilter filter, bool threaded) { BLI_assert_msg(new_size.x > 0 && new_size.y > 0, "Images must be at least 1 on both dimensions!"); if (ibuf == nullptr) { return nullptr; } /* Size same as source: just copy source image. */ const int2 src_size = int2(ibuf->x, ibuf->y); if (src_size == new_size) { ImBuf *dst = IMB_dupImBuf(ibuf); IMB_metadata_copy(dst, ibuf); return dst; } /* Allocate destination buffers. */ ImBufFlags flags = ImBufFlags::UninitializedPixels; if (ibuf->byte_data()) { flags |= ImBufFlags::ByteData; } if (ibuf->float_data()) { flags |= ImBufFlags::FloatData; } ImBuf *dst = IMB_allocImBuf(new_size.x, new_size.y, flags); dst->color_mode = ibuf->color_mode; dst->channels = ibuf->channels; IMB_metadata_copy(dst, ibuf); uchar *dst_byte = dst->byte_data_for_write(); float *dst_float = dst->float_data_for_write(); if (dst_byte == nullptr && dst_float == nullptr) { IMB_freeImBuf(dst); return nullptr; } switch (filter) { case IMBScaleFilter::Nearest: { if (const float *src = ibuf->float_data()) { scale_nearest_func(src, src_size, ibuf->channels, dst_float, new_size, threaded); } if (const uchar *src = ibuf->byte_data()) { scale_nearest_func(src, src_size, 4, dst_byte, new_size, threaded); } break; } case IMBScaleFilter::Bilinear: { if (const float *src = ibuf->float_data()) { scale_bilinear(src, src_size, ibuf->channels, dst_float, new_size, threaded); } if (const uchar *src = ibuf->byte_data()) { scale_bilinear(src, src_size, 4, dst_byte, new_size, threaded); } break; } case IMBScaleFilter::Box: { if (const float *src = ibuf->float_data()) { imb_scale_box(src, src_size, ibuf->channels, dst_float, new_size, threaded); } if (const uchar *src = ibuf->byte_data()) { imb_scale_box(src, src_size, 4, dst_byte, new_size, threaded); } break; } } dst->byte_buffer.colorspace = ibuf->byte_buffer.colorspace; dst->float_buffer.colorspace = ibuf->float_buffer.colorspace; return dst; } } // namespace blender