diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index f71d49db8ad8..28e6acbb8c81 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -160,8 +160,7 @@ def flush_images(): flush_images() -def _resample( - image_obj, data, out_shape, transform, *, resample=None, alpha=1): +def _resample(image_obj, data, out_shape, transform, *, resample=None, alpha=1): """ Convenience wrapper around `._image.resample` to resample *data* to *out_shape* (with a third dimension if *data* is RGBA) that takes care of @@ -204,7 +203,10 @@ def _resample( interpolation = 'nearest' else: interpolation = 'hanning' - out = np.zeros(out_shape + data.shape[2:], data.dtype) # 2D->2D, 3D->3D. + if len(data.shape) == 3: + # Always output RGBA. + out_shape += (4, ) + out = np.zeros(out_shape, data.dtype) if resample is None: resample = image_obj.get_resample() _image.resample(data, out, transform, @@ -216,20 +218,6 @@ def _resample( return out -def _rgb_to_rgba(A): - """ - Convert an RGB image to RGBA, as required by the image resample C++ - extension. - """ - rgba = np.zeros((A.shape[0], A.shape[1], 4), dtype=A.dtype) - rgba[:, :, :3] = A - if rgba.dtype == np.uint8: - rgba[:, :, 3] = 255 - else: - rgba[:, :, 3] = 1.0 - return rgba - - class _ImageBase(mcolorizer.ColorizingArtist): """ Base class for images. @@ -508,10 +496,10 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, # alpha channel below. output_alpha = (255 * alpha) if A.dtype == np.uint8 else alpha else: - output_alpha = _resample( # resample alpha channel - self, A[..., 3], out_shape, t, alpha=alpha) - output = _resample( # resample rgb channels - self, _rgb_to_rgba(A[..., :3]), out_shape, t, alpha=alpha) + # resample alpha channel + output_alpha = _resample(self, A[..., 3], out_shape, t, alpha=alpha) + # resample rgb channels + output = _resample(self, A[..., :3], out_shape, t, alpha=alpha) output[..., 3] = output_alpha # recombine rgb and alpha # output is now either a 2D array of normed (int or float) data diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index 24a0ab929bbf..e1d0e3e700d8 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -1577,8 +1577,8 @@ def test__resample_valid_output(): resample(np.zeros((9, 9)), np.zeros((9, 9, 4))) with pytest.raises(ValueError, match="different dimensionalities"): resample(np.zeros((9, 9, 4)), np.zeros((9, 9))) - with pytest.raises(ValueError, match="3D input array must be RGBA"): - resample(np.zeros((9, 9, 3)), np.zeros((9, 9, 4))) + with pytest.raises(ValueError, match="3D input array must be RGB"): + resample(np.zeros((9, 9, 2)), np.zeros((9, 9, 4))) with pytest.raises(ValueError, match="3D output array must be RGBA"): resample(np.zeros((9, 9, 4)), np.zeros((9, 9, 3))) with pytest.raises(ValueError, match="mismatched types"): diff --git a/src/_image_resample.h b/src/_image_resample.h index 19dc05b32e2a..66577ea4ae96 100644 --- a/src/_image_resample.h +++ b/src/_image_resample.h @@ -8,6 +8,7 @@ #include "agg_image_accessors.h" #include "agg_path_storage.h" #include "agg_pixfmt_gray.h" +#include "agg_pixfmt_rgb.h" #include "agg_pixfmt_rgba.h" #include "agg_renderer_base.h" #include "agg_renderer_scanline.h" @@ -16,6 +17,7 @@ #include "agg_span_allocator.h" #include "agg_span_converter.h" #include "agg_span_image_filter_gray.h" +#include "agg_span_image_filter_rgb.h" #include "agg_span_image_filter_rgba.h" #include "agg_span_interpolator_adaptor.h" #include "agg_span_interpolator_linear.h" @@ -496,16 +498,39 @@ typedef enum { } interpolation_e; -// T is rgba if and only if it has an T::r field. +// T is rgb(a) if and only if it has an T::r field. template struct is_grayscale : std::true_type {}; template struct is_grayscale> : std::false_type {}; template constexpr bool is_grayscale_v = is_grayscale::value; -template +// rgb_step is only used if input_has_alpha=false. +template struct type_mapping { - using blender_type = std::conditional_t< + using input_blender_type = std::conditional_t< + is_grayscale_v, + agg::blender_gray, + std::conditional_t< + input_has_alpha, + std::conditional_t< + std::is_same_v, + fixed_blender_rgba_plain, + agg::blender_rgba_plain + >, + agg::blender_rgb + > + >; + using input_pixfmt_type = std::conditional_t< + is_grayscale_v, + agg::pixfmt_alpha_blend_gray, + std::conditional_t< + input_has_alpha, + agg::pixfmt_alpha_blend_rgba, + agg::pixfmt_alpha_blend_rgb + > + >; + using output_blender_type = std::conditional_t< is_grayscale_v, agg::blender_gray, std::conditional_t< @@ -514,36 +539,37 @@ struct type_mapping agg::blender_rgba_plain > >; - using pixfmt_type = std::conditional_t< + using output_pixfmt_type = std::conditional_t< is_grayscale_v, - agg::pixfmt_alpha_blend_gray, - agg::pixfmt_alpha_blend_rgba - >; - using pixfmt_pre_type = std::conditional_t< - is_grayscale_v, - pixfmt_type, - agg::pixfmt_alpha_blend_rgba< - std::conditional_t< - std::is_same_v, - fixed_blender_rgba_pre, - agg::blender_rgba_pre - >, - agg::rendering_buffer> + agg::pixfmt_alpha_blend_gray, + agg::pixfmt_alpha_blend_rgba >; template using span_gen_affine_type = std::conditional_t< is_grayscale_v, agg::span_image_resample_gray_affine, - agg::span_image_resample_rgba_affine + std::conditional_t< + input_has_alpha, + agg::span_image_resample_rgba_affine, + agg::span_image_resample_rgb_affine + > >; template using span_gen_filter_type = std::conditional_t< is_grayscale_v, agg::span_image_filter_gray, - agg::span_image_filter_rgba + std::conditional_t< + input_has_alpha, + agg::span_image_filter_rgba, + agg::span_image_filter_rgb + > >; template using span_gen_nn_type = std::conditional_t< is_grayscale_v, agg::span_image_filter_gray_nn, - agg::span_image_filter_rgba_nn + std::conditional_t< + input_has_alpha, + agg::span_image_filter_rgba_nn, + agg::span_image_filter_rgb_nn + > >; }; @@ -697,16 +723,17 @@ static void get_filter(const resample_params_t ¶ms, } -template +// rgb_step is only used if input_has_alpha=false. +template void resample( - const void *input, int in_width, int in_height, - void *output, int out_width, int out_height, + const void *input, int in_width, int in_height, int in_stride, + void *output, int out_width, int out_height, int out_stride, resample_params_t ¶ms) { - using type_mapping_t = type_mapping; + using type_mapping_t = type_mapping; - using input_pixfmt_t = typename type_mapping_t::pixfmt_type; - using output_pixfmt_t = typename type_mapping_t::pixfmt_type; + using input_pixfmt_t = typename type_mapping_t::input_pixfmt_type; + using output_pixfmt_t = typename type_mapping_t::output_pixfmt_type; using renderer_t = agg::renderer_base; using rasterizer_t = agg::rasterizer_scanline_aa; @@ -722,11 +749,6 @@ void resample( using arbitrary_interpolator_t = agg::span_interpolator_adaptor, lookup_distortion>; - size_t itemsize = sizeof(color_type); - if (is_grayscale::value) { - itemsize /= 2; // agg::grayXX includes an alpha channel which we don't have. - } - if (params.interpolation != NEAREST && params.is_affine && fabs(params.affine.sx) == 1.0 && @@ -743,14 +765,12 @@ void resample( span_conv_alpha_t conv_alpha(params.alpha); agg::rendering_buffer input_buffer; - input_buffer.attach( - (unsigned char *)input, in_width, in_height, in_width * itemsize); + input_buffer.attach((unsigned char *)input, in_width, in_height, in_stride); input_pixfmt_t input_pixfmt(input_buffer); image_accessor_t input_accessor(input_pixfmt); agg::rendering_buffer output_buffer; - output_buffer.attach( - (unsigned char *)output, out_width, out_height, out_width * itemsize); + output_buffer.attach((unsigned char *)output, out_width, out_height, out_stride); output_pixfmt_t output_pixfmt(output_buffer); renderer_t renderer(output_pixfmt); diff --git a/src/_image_wrapper.cpp b/src/_image_wrapper.cpp index 0f7b0da88de8..5b30282b140a 100644 --- a/src/_image_wrapper.cpp +++ b/src/_image_wrapper.cpp @@ -106,14 +106,30 @@ image_resample(py::array input_array, throw std::invalid_argument("Input array must be a 2D or 3D array"); } - if (ndim == 3 && input_array.shape(2) != 4) { - throw std::invalid_argument( - "3D input array must be RGBA with shape (M, N, 4), has trailing dimension of {}"_s.format( - input_array.shape(2))); + py::ssize_t ncomponents = 0; + int rgb_step = 0; + if (ndim == 3) { + ncomponents = input_array.shape(2); + if (ncomponents == 3) { + // We special-case a few options in order to avoid copying in the common case. + auto rgb_stride = input_array.strides(1); + auto item_stride = input_array.strides(2); + if (rgb_stride == 3 * item_stride) { + rgb_step = 3; + } else if (rgb_stride == 4 * item_stride) { + rgb_step = 4; + } + } else if (ncomponents != 4) { + throw std::invalid_argument( + "3D input array must be RGB with shape (M, N, 3) or RGBA with shape (M, N, 4), " + "has trailing dimension of {}"_s.format(ncomponents)); + } } - // Ensure input array is contiguous, regardless of dtype - input_array = py::array::ensure(input_array, py::array::c_style); + if (rgb_step == 0) { + // Ensure input array is contiguous, regardless of dtype + input_array = py::array::ensure(input_array, py::array::c_style); + } // Validate output array auto out_ndim = output_array.ndim(); @@ -173,25 +189,44 @@ image_resample(py::array input_array, if (auto resampler = (ndim == 2) ? ( - (dtype.equal(py::dtype::of())) ? resample : - (dtype.equal(py::dtype::of())) ? resample : - (dtype.equal(py::dtype::of())) ? resample : - (dtype.equal(py::dtype::of())) ? resample : - (dtype.equal(py::dtype::of())) ? resample : - (dtype.equal(py::dtype::of())) ? resample : + dtype.equal(py::dtype::of()) ? resample : + dtype.equal(py::dtype::of()) ? resample : + dtype.equal(py::dtype::of()) ? resample : + dtype.equal(py::dtype::of()) ? resample : + dtype.equal(py::dtype::of()) ? resample : + dtype.equal(py::dtype::of()) ? resample : nullptr) : ( - // ndim == 3 - (dtype.equal(py::dtype::of())) ? resample : - (dtype.equal(py::dtype::of())) ? resample : - (dtype.equal(py::dtype::of())) ? resample : - (dtype.equal(py::dtype::of())) ? resample : - (dtype.equal(py::dtype::of())) ? resample : - (dtype.equal(py::dtype::of())) ? resample : - nullptr)) { + // ndim == 3 + (ncomponents == 4) ? ( + dtype.equal(py::dtype::of()) ? resample : + dtype.equal(py::dtype::of()) ? resample : + dtype.equal(py::dtype::of()) ? resample : + dtype.equal(py::dtype::of()) ? resample : + dtype.equal(py::dtype::of()) ? resample : + dtype.equal(py::dtype::of()) ? resample : + nullptr + ) : ( + (rgb_step == 4) ? ( + dtype.equal(py::dtype::of()) ? resample : + dtype.equal(py::dtype::of()) ? resample : + dtype.equal(py::dtype::of()) ? resample : + dtype.equal(py::dtype::of()) ? resample : + dtype.equal(py::dtype::of()) ? resample : + dtype.equal(py::dtype::of()) ? resample : + nullptr + ) : ( + dtype.equal(py::dtype::of()) ? resample : + dtype.equal(py::dtype::of()) ? resample : + dtype.equal(py::dtype::of()) ? resample : + dtype.equal(py::dtype::of()) ? resample : + dtype.equal(py::dtype::of()) ? resample : + dtype.equal(py::dtype::of()) ? resample : + nullptr)))) + { Py_BEGIN_ALLOW_THREADS resampler( - input_array.data(), input_array.shape(1), input_array.shape(0), - output_array.mutable_data(), output_array.shape(1), output_array.shape(0), + input_array.data(), input_array.shape(1), input_array.shape(0), input_array.strides(0), + output_array.mutable_data(), output_array.shape(1), output_array.shape(0), output_array.strides(0), params); Py_END_ALLOW_THREADS } else { diff --git a/src/agg_workaround.h b/src/agg_workaround.h index 476219519280..a167be97e171 100644 --- a/src/agg_workaround.h +++ b/src/agg_workaround.h @@ -8,46 +8,6 @@ blending of RGBA32 pixels does not preserve enough precision */ -template -struct fixed_blender_rgba_pre : agg::conv_rgba_pre -{ - typedef ColorT color_type; - typedef Order order_type; - typedef typename color_type::value_type value_type; - typedef typename color_type::calc_type calc_type; - typedef typename color_type::long_type long_type; - enum base_scale_e - { - base_shift = color_type::base_shift, - base_mask = color_type::base_mask - }; - - //-------------------------------------------------------------------- - static AGG_INLINE void blend_pix(value_type* p, - value_type cr, value_type cg, value_type cb, - value_type alpha, agg::cover_type cover) - { - blend_pix(p, - color_type::mult_cover(cr, cover), - color_type::mult_cover(cg, cover), - color_type::mult_cover(cb, cover), - color_type::mult_cover(alpha, cover)); - } - - //-------------------------------------------------------------------- - static AGG_INLINE void blend_pix(value_type* p, - value_type cr, value_type cg, value_type cb, - value_type alpha) - { - alpha = base_mask - alpha; - p[Order::R] = (value_type)(((p[Order::R] * alpha) >> base_shift) + cr); - p[Order::G] = (value_type)(((p[Order::G] * alpha) >> base_shift) + cg); - p[Order::B] = (value_type)(((p[Order::B] * alpha) >> base_shift) + cb); - p[Order::A] = (value_type)(base_mask - ((alpha * (base_mask - p[Order::A])) >> base_shift)); - } -}; - - template struct fixed_blender_rgba_plain : agg::conv_rgba_plain {