From 723fb163436b2d5f1fbe835f84ba018307c30a0e Mon Sep 17 00:00:00 2001 From: Jeroen Bakker Date: Wed, 15 Dec 2021 11:09:31 +0100 Subject: Images: 1,2,3 channel support for transform function. Added support for 1, 2, 3 float channel source images. Destination images must still be 4 channels. --- source/blender/imbuf/IMB_imbuf.h | 19 ++ source/blender/imbuf/intern/transform.cc | 472 ++++++++++++++++++++++++++++--- 2 files changed, 446 insertions(+), 45 deletions(-) diff --git a/source/blender/imbuf/IMB_imbuf.h b/source/blender/imbuf/IMB_imbuf.h index f71eef66a62..a0236b8ec4d 100644 --- a/source/blender/imbuf/IMB_imbuf.h +++ b/source/blender/imbuf/IMB_imbuf.h @@ -908,6 +908,25 @@ typedef enum eIMBTransformMode { IMB_TRANSFORM_MODE_WRAP_REPEAT = 2, } eIMBTransformMode; +/** + * \brief Transform source image buffer onto destination image buffer using a transform matrix. + * + * \param src Image buffer to read from. + * \param dst Image buffer to write to. rect or rect_float must already be initialized. + * - dst buffer must be a 4 channel buffers. + * - 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 filter Interpolation to use during sampling. + * \param transform_matrix Transformation matrix to use. + * The given matrix should transform between dst texel space to src texel space. + * One unit is one texel. + * \param src_crop cropping region how to crop the source buffer. Should only be passed when mode + * is set to IMB_TRANSFORM_MODE_CROP_SRC. For any other mode this should be empty. + * + * During transformation no data/color conversion will happens. + * When transforming between float images the number of channels of the source buffer may be + * between 1 and 4. When source buffer has one channel the data will be read as a grey scale value. + */ void IMB_transform(const struct ImBuf *src, struct ImBuf *dst, const eIMBTransformMode mode, diff --git a/source/blender/imbuf/intern/transform.cc b/source/blender/imbuf/intern/transform.cc index 0cb363c3bce..dc28d85eeea 100644 --- a/source/blender/imbuf/intern/transform.cc +++ b/source/blender/imbuf/intern/transform.cc @@ -21,6 +21,7 @@ * \ingroup imbuf */ +#include #include #include "BLI_math.h" @@ -32,13 +33,33 @@ namespace blender::imbuf::transform { struct TransformUserData { + /** \brief Source image buffer to read from. */ const ImBuf *src; + /** \brief Destination image buffer to write to. */ ImBuf *dst; + /** \brief UV coordinates at the origin (0,0) in source image space. */ float start_uv[2]; + + /** + * \brief delta UV coordinates along the source image buffer, when moving a single texel in the X + * axis of the dst image buffer. + */ float add_x[2]; + + /** + * \brief delta UV coordinate along the source image buffer, when moving a single texel in the Y + * axes of the dst image buffer. + */ float add_y[2]; + + /** + * \brief Cropping region in source image texel space. + */ rctf src_crop; + /** + * \brief Initialize the start_uv, add_x and add_y fields based on the given transform matrix. + */ void init(const float transform_matrix[4][4]) { init_start_uv(transform_matrix); @@ -85,24 +106,369 @@ struct TransformUserData { } }; -template -class ScanlineProcessor { +/** + * \brief Base class for source discarding. + * + * The class decides if a specific uv coordinate from the source buffer should be ignored. + * This is used to mix multiple images over a single output buffer. Discarded pixels will + * not change the output buffer. + */ +class BaseDiscard { + public: + virtual ~BaseDiscard() = default; + + /** + * \brief Should the source pixel at the given uv coordinate be discarded. + */ + virtual bool should_discard(const TransformUserData &user_data, const float uv[2]) = 0; +}; + +/** + * \brief Crop uv-coordinates that are outside the user data src_crop rect. + */ +class CropSource : public BaseDiscard { + public: + /** + * \brief Should the source pixel at the given uv coordinate be discarded. + * + * Uses user_data.src_crop to determine if the uv coordinate should be skipped. + */ + bool should_discard(const TransformUserData &user_data, const float uv[2]) override + { + return uv[0] < user_data.src_crop.xmin || uv[0] >= user_data.src_crop.xmax || + uv[1] < user_data.src_crop.ymin || uv[1] >= user_data.src_crop.ymax; + } +}; + +/** + * \brief Discard that does not discard anything. + */ +class NoDiscard : public BaseDiscard { + public: + /** + * \brief Should the source pixel at the given uv coordinate be discarded. + * + * Will never discard any pixels. + */ + bool should_discard(const TransformUserData &UNUSED(user_data), + const float UNUSED(uv[2])) override + { + return false; + } +}; + +/** + * \brief Pointer to a texel to write to in serial. + */ +template< + /** + * \brief Kind of buffer. + * Possible options: float, unsigned char. + */ + typename StorageType = float, + + /** + * \brief Number of channels of a single pixel. + */ + int NumChannels = 4> +class TexelPointer { + public: + static const int ChannelLen = NumChannels; + private: - void pixel_from_buffer(const struct ImBuf *ibuf, unsigned char **outI, float **outF, int y) const + StorageType *pointer; + + public: + void init_pixel_pointer(const ImBuf *image_buffer, int x, int y) + { + const size_t offset = (y * (size_t)image_buffer->x + x) * NumChannels; + + if constexpr (std::is_same_v) { + pointer = image_buffer->rect_float + offset; + } + else if constexpr (std::is_same_v) { + pointer = const_cast( + static_cast(static_cast(image_buffer->rect)) + + offset); + } + else { + pointer = nullptr; + } + } + + /** + * \brief Get pointer to the current texel to write to. + */ + StorageType *get_pointer() + { + return pointer; + } + + void increase_pixel_pointer() + { + pointer += NumChannels; + } +}; + +/** + * \brief Wrapping mode for the uv coordinates. + * + * Subclasses have the ability to change the UV coordinates when sampling the source buffer. + */ +class BaseUVWrapping { + public: + /** + * \brief modify the given u coordinate. + */ + virtual float modify_u(const ImBuf *source_buffer, float u) = 0; + + /** + * \brief modify the given v coordinate. + */ + virtual float modify_v(const ImBuf *source_buffer, float v) = 0; +}; + +/** + * \brief UVWrapping method that does not modify the UV coordinates. + */ +class PassThroughUV : public BaseUVWrapping { + public: + float modify_u(const ImBuf *UNUSED(source_buffer), float u) override + { + return u; + } + + float modify_v(const ImBuf *UNUSED(source_buffer), float v) override + { + return v; + } +}; + +/** + * \brief UVWrapping method that wrap repeats the UV coordinates. + */ +class WrapRepeatUV : public BaseUVWrapping { + public: + float modify_u(const ImBuf *source_buffer, float u) override + + { + int x = (int)floor(u); + x = x % source_buffer->x; + if (x < 0) { + x += source_buffer->x; + } + return x; + } + + float modify_v(const ImBuf *source_buffer, float v) override + { + int y = (int)floor(v); + y = y % source_buffer->y; + if (y < 0) { + y += source_buffer->y; + } + return y; + } +}; +/** + * \brief Read a sample from an image buffer. + * + * A sampler can read from an image buffer. + * + */ +template< + /** \brief Interpolation mode to use when sampling. */ + eIMBInterpolationFilterMode Filter, + + /** \brief storage type of a single texel channel (unsigned char or float). */ + typename StorageType, + /** + * \brief number of channels if the image to read. + * + * Must match the actual channels of the image buffer that is sampled. + */ + int NumChannels, + /** + * \brief Wrapping method to perform + * + * Should be a subclass of BaseUVWrapper + */ + typename UVWrapping> +class Sampler { + UVWrapping uv_wrapper; + + public: + using ChannelType = StorageType; + static const int ChannelLen = NumChannels; + using SampleType = std::array; + + void sample(const ImBuf *source, const float u, const float v, SampleType &r_sample) { - const size_t offset = ((size_t)ibuf->x) * y * ChannelLen; + const float wrapped_u = uv_wrapper.modify_u(source, u); + const float wrapped_v = uv_wrapper.modify_v(source, v); - if (ibuf->rect) { - *outI = (unsigned char *)ibuf->rect + offset; + if constexpr (Filter == IMB_FILTER_BILINEAR && std::is_same_v && + NumChannels == 4) { + bilinear_interpolation_color_fl(source, nullptr, r_sample.begin(), wrapped_u, wrapped_v); + } + else if constexpr (Filter == IMB_FILTER_NEAREST && + std::is_same_v && NumChannels == 4) { + nearest_interpolation_color_char(source, r_sample.begin(), nullptr, wrapped_u, wrapped_v); + } + else if constexpr (Filter == IMB_FILTER_BILINEAR && + std::is_same_v && NumChannels == 4) { + bilinear_interpolation_color_char(source, r_sample.begin(), nullptr, wrapped_u, wrapped_v); + } + else if constexpr (Filter == IMB_FILTER_BILINEAR && std::is_same_v) { + if constexpr (std::is_same_v) { + BLI_bilinear_interpolation_wrap_fl(source->rect_float, + r_sample.begin(), + source->x, + source->y, + NumChannels, + u, + v, + true, + true); + } + else { + BLI_bilinear_interpolation_fl(source->rect_float, + r_sample.begin(), + source->x, + source->y, + NumChannels, + wrapped_u, + wrapped_v); + } + } + else if constexpr (Filter == IMB_FILTER_NEAREST && std::is_same_v) { + sample_nearest_float(source, wrapped_u, wrapped_v, r_sample); + } + else { + /* Unsupported sampler. */ + BLI_assert_unreachable(); + } + } + + private: + void sample_nearest_float(const ImBuf *source, + const float u, + const float v, + SampleType &r_sample) + { + BLI_STATIC_ASSERT(std::is_same_v); + + /* ImBuf in must have a valid rect or rect_float, assume this is already checked */ + int x1 = (int)(u); + int y1 = (int)(v); + + /* Break when sample outside image is requested. */ + if (x1 < 0 || x1 >= source->x || y1 < 0 || y1 >= source->y) { + for (int i = 0; i < NumChannels; i++) { + r_sample[i] = 0.0f; + } + return; } - if (ibuf->rect_float) { - *outF = ibuf->rect_float + offset; + const size_t offset = ((size_t)source->x * y1 + x1) * NumChannels; + const float *dataF = source->rect_float + offset; + for (int i = 0; i < NumChannels; i++) { + r_sample[i] = dataF[i]; } } +}; +/** + * \brief Change the number of channels and store it. + * + * Template class to convert and store a sample in a TexelPointer. + * It supports: + * - 4 channel unsigned char -> 4 channel unsigned char. + * - 4 channel float -> 4 channel float. + * - 3 channel float -> 4 channel float. + * - 2 channel float -> 4 channel float. + * - 1 channel float -> 4 channel float. + */ +template +class ChannelConverter { public: + using SampleType = std::array; + using TexelType = TexelPointer; + + /** + * \brief Convert the number of channels of the given sample to match the texel pointer and store + * it at the location the texel_pointer points at. + */ + void convert_and_store(const SampleType &sample, TexelType &texel_pointer) + { + if constexpr (std::is_same_v) { + BLI_STATIC_ASSERT(SourceNumChannels == 4, "Unsigned chars always have 4 channels."); + BLI_STATIC_ASSERT(DestinationNumChannels == 4, "Unsigned chars always have 4 channels."); + + copy_v4_v4_uchar(texel_pointer.get_pointer(), sample.begin()); + } + else if constexpr (std::is_same_v && SourceNumChannels == 4 && + DestinationNumChannels == 4) { + copy_v4_v4(texel_pointer.get_pointer(), sample.begin()); + } + else if constexpr (std::is_same_v && SourceNumChannels == 3 && + DestinationNumChannels == 4) { + copy_v4_fl4(texel_pointer.get_pointer(), sample[0], sample[1], sample[2], 1.0f); + } + else if constexpr (std::is_same_v && SourceNumChannels == 2 && + DestinationNumChannels == 4) { + copy_v4_fl4(texel_pointer.get_pointer(), sample[0], sample[1], 0.0f, 1.0f); + } + else if constexpr (std::is_same_v && SourceNumChannels == 1 && + DestinationNumChannels == 4) { + copy_v4_fl4(texel_pointer.get_pointer(), sample[0], sample[0], sample[0], 1.0f); + } + else { + BLI_assert_unreachable(); + } + } +}; + +/** + * \brief Processor for a scanline. + */ +template< + /** + * \brief Discard function to use. + * + * \attention Should be a subclass of BaseDiscard. + */ + typename Discard, + + /** + * \brief Color interpolation function to read from the source buffer. + */ + typename Sampler, + + /** + * \brief Kernel to store to the destination buffer. + * Should be an TexelPointer + */ + typename OutputTexelPointer> +class ScanlineProcessor { + Discard discarder; + OutputTexelPointer output; + Sampler sampler; + + /** + * \brief Channels sizzling logic to convert between the input image buffer and the output image + * buffer. + */ + ChannelConverter + channel_converter; + + public: + /** + * \brief Inner loop of the transformations, processing a full scanline. + */ void process(const TransformUserData *user_data, int scanline) { const int width = user_data->dst->x; @@ -110,31 +476,23 @@ class ScanlineProcessor { float uv[2]; madd_v2_v2v2fl(uv, user_data->start_uv, user_data->add_y, scanline); - unsigned char *outI = nullptr; - float *outF = nullptr; - pixel_from_buffer(user_data->dst, &outI, &outF, scanline); - + output.init_pixel_pointer(user_data->dst, 0, scanline); for (int xi = 0; xi < width; xi++) { - if constexpr (Mode == IMB_TRANSFORM_MODE_CROP_SRC) { - if (uv[0] >= user_data->src_crop.xmin && uv[0] < user_data->src_crop.xmax && - uv[1] >= user_data->src_crop.ymin && uv[1] < user_data->src_crop.ymax) { - ColorInterpolation(user_data->src, outI, outF, uv[0], uv[1]); - } - } - else { - ColorInterpolation(user_data->src, outI, outF, uv[0], uv[1]); + if (!discarder.should_discard(*user_data, uv)) { + typename Sampler::SampleType sample; + sampler.sample(user_data->src, uv[0], uv[1], sample); + channel_converter.convert_and_store(sample, output); } + add_v2_v2(uv, user_data->add_x); - if (outI) { - outI += ChannelLen; - } - if (outF) { - outF += ChannelLen; - } + output.increase_pixel_pointer(); } } }; +/** + * \brief callback function for threaded transformation. + */ template void transform_scanline_function(void *custom_data, int scanline) { const TransformUserData *user_data = static_cast(custom_data); @@ -142,20 +500,29 @@ template void transform_scanline_function(void *custom_data, processor.process(user_data, scanline); } -template +template ScanlineThreadFunc get_scanline_function(const eIMBTransformMode mode) { switch (mode) { case IMB_TRANSFORM_MODE_REGULAR: return transform_scanline_function< - ScanlineProcessor>; + ScanlineProcessor, + TexelPointer>>; case IMB_TRANSFORM_MODE_CROP_SRC: return transform_scanline_function< - ScanlineProcessor>; + ScanlineProcessor, + TexelPointer>>; case IMB_TRANSFORM_MODE_WRAP_REPEAT: return transform_scanline_function< - ScanlineProcessor>; + ScanlineProcessor, + TexelPointer>>; } BLI_assert_unreachable(); @@ -163,23 +530,38 @@ ScanlineThreadFunc get_scanline_function(const eIMBTransformMode mode) } template -static void transform(TransformUserData *user_data, const eIMBTransformMode mode) +ScanlineThreadFunc get_scanline_function(const TransformUserData *user_data, + const eIMBTransformMode mode) +{ + const ImBuf *src = user_data->src; + const ImBuf *dst = user_data->dst; + + if (src->channels == 4 && dst->channels == 4) { + return get_scanline_function(mode); + } + if (src->channels == 3 && dst->channels == 4) { + return get_scanline_function(mode); + } + if (src->channels == 2 && dst->channels == 4) { + return get_scanline_function(mode); + } + if (src->channels == 1 && dst->channels == 4) { + return get_scanline_function(mode); + } + return nullptr; +} + +template +static void transform_threaded(TransformUserData *user_data, const eIMBTransformMode mode) { ScanlineThreadFunc scanline_func = nullptr; - if (user_data->dst->rect_float) { - constexpr InterpolationColorFunction interpolation_function = - Filter == IMB_FILTER_NEAREST ? nearest_interpolation_color_fl : - bilinear_interpolation_color_fl; - scanline_func = - get_scanline_function(mode); + if (user_data->dst->rect_float && user_data->src->rect_float) { + scanline_func = get_scanline_function(user_data, mode); } - else if (user_data->dst->rect) { - constexpr InterpolationColorFunction interpolation_function = - Filter == IMB_FILTER_NEAREST ? nearest_interpolation_color_char : - bilinear_interpolation_color_char; - scanline_func = - get_scanline_function(mode); + else if (user_data->dst->rect && user_data->src->rect) { + /* Number of channels is always 4 when using unsigned char buffers (sRGB + straight alpha). */ + scanline_func = get_scanline_function(mode); } if (scanline_func != nullptr) { @@ -213,10 +595,10 @@ void IMB_transform(const struct ImBuf *src, user_data.init(transform_matrix); if (filter == IMB_FILTER_NEAREST) { - transform(&user_data, mode); + transform_threaded(&user_data, mode); } else { - transform(&user_data, mode); + transform_threaded(&user_data, mode); } } } -- cgit v1.2.3