#include #include #include "_image_resample.h" #include "py_converters.h" namespace py = pybind11; using namespace pybind11::literals; /********************************************************************** * Free functions * */ const char* image_resample__doc__ = R"""(Resample input_array, blending it in-place into output_array, using an affine transform. Parameters ---------- input_array : 2-d or 3-d NumPy array of float, double or `numpy.uint8` If 2-d, the image is grayscale. If 3-d, the image must be of size 4 in the last dimension and represents RGBA data. output_array : 2-d or 3-d NumPy array of float, double or `numpy.uint8` The dtype and number of dimensions must match `input_array`. transform : matplotlib.transforms.Transform instance The transformation from the input array to the output array. interpolation : int, default: NEAREST The interpolation method. Must be one of the following constants defined in this module: NEAREST, BILINEAR, BICUBIC, SPLINE16, SPLINE36, HANNING, HAMMING, HERMITE, KAISER, QUADRIC, CATROM, GAUSSIAN, BESSEL, MITCHELL, SINC, LANCZOS, BLACKMAN resample : bool, optional When `True`, use a full resampling method. When `False`, only resample when the output image is larger than the input image. alpha : float, default: 1 The transparency level, from 0 (transparent) to 1 (opaque). norm : bool, default: False Whether to norm the interpolation function. radius: float, default: 1 The radius of the kernel, if method is SINC, LANCZOS or BLACKMAN. )"""; static py::array_t _get_transform_mesh(const py::object& transform, const py::ssize_t *dims) { /* TODO: Could we get away with float, rather than double, arrays here? */ /* Given a non-affine transform object, create a mesh that maps every pixel in the output image to the input image. This is used as a lookup table during the actual resampling. */ // If attribute doesn't exist, raises Python AttributeError auto inverse = transform.attr("inverted")(); py::ssize_t mesh_dims[2] = {dims[0]*dims[1], 2}; py::array_t input_mesh(mesh_dims); auto p = input_mesh.mutable_data(); for (auto y = 0; y < dims[0]; ++y) { for (auto x = 0; x < dims[1]; ++x) { *p++ = (double)x; *p++ = (double)y; } } auto output_mesh = inverse.attr("transform")(input_mesh); auto output_mesh_array = py::array_t(output_mesh); if (output_mesh_array.ndim() != 2) { throw std::runtime_error( "Inverse transformed mesh array should be 2D not {}D"_s.format( output_mesh_array.ndim())); } return output_mesh_array; } // Using generic py::array for input and output arrays rather than the more usual // py::array_t as this function supports multiple array dtypes. static void image_resample(py::array input_array, py::array& output_array, const py::object& transform, interpolation_e interpolation, bool resample_, // Avoid name clash with resample() function float alpha, bool norm, float radius) { // Validate input_array auto dtype = input_array.dtype(); // Validated when determine resampler below auto ndim = input_array.ndim(); if (ndim != 2 && ndim != 3) { 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))); } // 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(); if (out_ndim != ndim) { throw std::invalid_argument( "Input ({}D) and output ({}D) arrays have different dimensionalities"_s.format( ndim, out_ndim)); } if (out_ndim == 3 && output_array.shape(2) != 4) { throw std::invalid_argument( "3D output array must be RGBA with shape (M, N, 4), has trailing dimension of {}"_s.format( output_array.shape(2))); } if (!output_array.dtype().is(dtype)) { throw std::invalid_argument("Input and output arrays have mismatched types"); } if ((output_array.flags() & py::array::c_style) == 0) { throw std::invalid_argument("Output array must be C-contiguous"); } if (!output_array.writeable()) { throw std::invalid_argument("Output array must be writeable"); } resample_params_t params; params.interpolation = interpolation; params.transform_mesh = nullptr; params.resample = resample_; params.norm = norm; params.radius = radius; params.alpha = alpha; // Only used if transform is not affine. // Need to keep it in scope for the duration of this function. py::array_t transform_mesh; // Validate transform if (transform.is_none()) { params.is_affine = true; } else { // Raises Python AttributeError if no such attribute or TypeError if cast fails bool is_affine = py::cast(transform.attr("is_affine")); if (is_affine) { convert_trans_affine(transform, params.affine); params.is_affine = true; } else { transform_mesh = _get_transform_mesh(transform, output_array.shape()); params.transform_mesh = transform_mesh.data(); params.is_affine = false; } } 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 : 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)) { 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), params); Py_END_ALLOW_THREADS } else { throw std::invalid_argument("arrays must be of dtype byte, short, float32 or float64"); } } PYBIND11_MODULE(_image, m, py::mod_gil_not_used()) { py::enum_(m, "_InterpolationType") .value("NEAREST", NEAREST) .value("BILINEAR", BILINEAR) .value("BICUBIC", BICUBIC) .value("SPLINE16", SPLINE16) .value("SPLINE36", SPLINE36) .value("HANNING", HANNING) .value("HAMMING", HAMMING) .value("HERMITE", HERMITE) .value("KAISER", KAISER) .value("QUADRIC", QUADRIC) .value("CATROM", CATROM) .value("GAUSSIAN", GAUSSIAN) .value("BESSEL", BESSEL) .value("MITCHELL", MITCHELL) .value("SINC", SINC) .value("LANCZOS", LANCZOS) .value("BLACKMAN", BLACKMAN) .export_values(); m.def("resample", &image_resample, "input_array"_a, "output_array"_a, "transform"_a, "interpolation"_a = interpolation_e::NEAREST, "resample"_a = false, "alpha"_a = 1, "norm"_a = false, "radius"_a = 1, image_resample__doc__); }