diff --git a/doc/api/next_api_changes/behavior/14913-AL.rst b/doc/api/next_api_changes/behavior/14913-AL.rst new file mode 100644 index 000000000000..9e98b7e6c8ec --- /dev/null +++ b/doc/api/next_api_changes/behavior/14913-AL.rst @@ -0,0 +1,9 @@ +The output of ``NonUniformImage`` and ``PcolorImage`` has changed +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Pixel-level differences may be observed in images generated using +`.NonUniformImage` or `.PcolorImage`, typically for pixels exactly at the +boundary between two data cells (no user-facing axes method currently generates +`.NonUniformImage`\s, and only `.pcolorfast` can generate `.PcolorImage`\s). +These artists are also now slower, normally by ~1.5x but sometimes more (in +particular for ``NonUniformImage(interpolation="bilinear")``. This slowdown +arises from fixing occasional floating point inaccuracies. diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index e755b204ab9d..bdbeba43cb15 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -1055,14 +1055,51 @@ def make_image(self, renderer, magnification=1.0, unsampled=False): self._is_grayscale = False vl = self.axes.viewLim l, b, r, t = self.axes.bbox.extents - width = (round(r) + 0.5) - (round(l) - 0.5) - height = (round(t) + 0.5) - (round(b) - 0.5) - width *= magnification - height *= magnification - im = _image.pcolor(self._Ax, self._Ay, A, - int(height), int(width), - (vl.x0, vl.x1, vl.y0, vl.y1), - _interpd_[self._interpolation]) + width = int(((round(r) + 0.5) - (round(l) - 0.5)) * magnification) + height = int(((round(t) + 0.5) - (round(b) - 0.5)) * magnification) + x_pix = np.linspace(vl.x0, vl.x1, width) + y_pix = np.linspace(vl.y0, vl.y1, height) + if self._interpolation == "nearest": + x_mid = (self._Ax[:-1] + self._Ax[1:]) / 2 + y_mid = (self._Ay[:-1] + self._Ay[1:]) / 2 + x_int = x_mid.searchsorted(x_pix) + y_int = y_mid.searchsorted(y_pix) + # The following is equal to `A[y_int[:, None], x_int[None, :]]`, + # but many times faster. Both casting to uint32 (to have an + # effectively 1D array) and manual index flattening matter. + im = ( + np.ascontiguousarray(A).view(np.uint32).ravel()[ + np.add.outer(y_int * A.shape[1], x_int)] + .view(np.uint8).reshape((height, width, 4))) + else: # self._interpolation == "bilinear" + # Use np.interp to compute x_int/x_float has similar speed. + x_int = np.clip( + self._Ax.searchsorted(x_pix) - 1, 0, len(self._Ax) - 2) + y_int = np.clip( + self._Ay.searchsorted(y_pix) - 1, 0, len(self._Ay) - 2) + idx_int = np.add.outer(y_int * A.shape[1], x_int) + x_frac = np.clip( + np.divide(x_pix - self._Ax[x_int], np.diff(self._Ax)[x_int], + dtype=np.float32), # Downcasting helps with speed. + 0, 1) + y_frac = np.clip( + np.divide(y_pix - self._Ay[y_int], np.diff(self._Ay)[y_int], + dtype=np.float32), + 0, 1) + f00 = np.outer(1 - y_frac, 1 - x_frac) + f10 = np.outer(y_frac, 1 - x_frac) + f01 = np.outer(1 - y_frac, x_frac) + f11 = np.outer(y_frac, x_frac) + im = np.empty((height, width, 4), np.uint8) + for chan in range(4): + ac = A[:, :, chan].reshape(-1) # reshape(-1) avoids a copy. + # Shifting the buffer start (`ac[offset:]`) avoids an array + # addition (`ac[idx_int + offset]`). + buf = f00 * ac[idx_int] + buf += f10 * ac[A.shape[1]:][idx_int] + buf += f01 * ac[1:][idx_int] + buf += f11 * ac[A.shape[1] + 1:][idx_int] + im[:, :, chan] = buf # Implicitly casts to uint8. return im, l, b, IdentityTransform() def set_data(self, x, y, A): @@ -1186,27 +1223,33 @@ def make_image(self, renderer, magnification=1.0, unsampled=False): raise RuntimeError('You must first set the image array') if unsampled: raise ValueError('unsampled not supported on PColorImage') - fc = self.axes.patch.get_facecolor() - bg = mcolors.to_rgba(fc, 0) - bg = (np.array(bg)*255).astype(np.uint8) + + if self._rgbacache is None: + A = self.to_rgba(self._A, bytes=True) + self._rgbacache = np.pad(A, [(1, 1), (1, 1), (0, 0)], "constant") + if self._A.ndim == 2: + self._is_grayscale = self.cmap.is_gray() + padded_A = self._rgbacache + bg = mcolors.to_rgba(self.axes.patch.get_facecolor(), 0) + bg = (np.array(bg) * 255).astype(np.uint8) + if (padded_A[0, 0] != bg).all(): + padded_A[[0, -1], :] = padded_A[:, [0, -1]] = bg + l, b, r, t = self.axes.bbox.extents width = (round(r) + 0.5) - (round(l) - 0.5) height = (round(t) + 0.5) - (round(b) - 0.5) width = int(round(width * magnification)) height = int(round(height * magnification)) - if self._rgbacache is None: - A = self.to_rgba(self._A, bytes=True) - self._rgbacache = A - if self._A.ndim == 2: - self._is_grayscale = self.cmap.is_gray() - else: - A = self._rgbacache vl = self.axes.viewLim - im = _image.pcolor2(self._Ax, self._Ay, A, - height, - width, - (vl.x0, vl.x1, vl.y0, vl.y1), - bg) + + x_pix = np.linspace(vl.x0, vl.x1, width) + y_pix = np.linspace(vl.y0, vl.y1, height) + x_int = self._Ax.searchsorted(x_pix) + y_int = self._Ay.searchsorted(y_pix) + im = ( # See comment in NonUniformImage.make_image re: performance. + padded_A.view(np.uint32).ravel()[ + np.add.outer(y_int * padded_A.shape[1], x_int)] + .view(np.uint8).reshape((height, width, 4))) return im, l, b, IdentityTransform() def _check_unsampled_image(self): diff --git a/lib/matplotlib/tests/baseline_images/test_image/nonuniform_and_pcolor.png b/lib/matplotlib/tests/baseline_images/test_image/nonuniform_and_pcolor.png new file mode 100644 index 000000000000..cb0aa4815c4e Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_image/nonuniform_and_pcolor.png differ diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index 36ad1aadaa4c..418c0b2219ec 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -1263,3 +1263,19 @@ def test_spy_box(fig_test, fig_ref): ax_ref[i].yaxis.set_major_locator( mticker.MaxNLocator(nbins=9, steps=[1, 2, 5, 10], integer=True) ) + + +@image_comparison(["nonuniform_and_pcolor.png"], style="mpl20") +def test_nonuniform_and_pcolor(): + axs = plt.figure(figsize=(3, 3)).subplots(3, sharex=True, sharey=True) + for ax, interpolation in zip(axs, ["nearest", "bilinear"]): + im = NonUniformImage(ax, interpolation=interpolation) + im.set_data(np.arange(3) ** 2, np.arange(3) ** 2, + np.arange(9).reshape((3, 3))) + ax.add_image(im) + axs[2].pcolorfast( # PcolorImage + np.arange(4) ** 2, np.arange(4) ** 2, np.arange(9).reshape((3, 3))) + for ax in axs: + ax.set_axis_off() + # NonUniformImage "leaks" out of extents, not PColorImage. + ax.set(xlim=(0, 10)) diff --git a/setupext.py b/setupext.py index 476d426cf8a5..18df0cac9f90 100644 --- a/setupext.py +++ b/setupext.py @@ -413,7 +413,6 @@ def get_extensions(self): # image ext = Extension( "matplotlib._image", [ - "src/_image.cpp", "src/mplutils.cpp", "src/_image_wrapper.cpp", "src/py_converters.cpp", diff --git a/src/_image.cpp b/src/_image.cpp deleted file mode 100644 index 28e509a4a445..000000000000 --- a/src/_image.cpp +++ /dev/null @@ -1,118 +0,0 @@ -/* -*- mode: c++; c-basic-offset: 4 -*- */ - -#define NO_IMPORT_ARRAY - -#include - -// utilities for irregular grids -void _bin_indices_middle( - unsigned int *irows, int nrows, const float *ys1, unsigned long ny, float dy, float y_min) -{ - int i, j, j_last; - unsigned int *rowstart = irows; - const float *ys2 = ys1 + 1; - const float *yl = ys1 + ny; - float yo = y_min + dy / 2.0f; - float ym = 0.5f * (*ys1 + *ys2); - // y/rows - j = 0; - j_last = j; - for (i = 0; i < nrows; i++, yo += dy, rowstart++) { - while (ys2 != yl && yo > ym) { - ys1 = ys2; - ys2 = ys1 + 1; - ym = 0.5f * (*ys1 + *ys2); - j++; - } - *rowstart = j - j_last; - j_last = j; - } -} - -void _bin_indices_middle_linear(float *arows, - unsigned int *irows, - int nrows, - const float *y, - unsigned long ny, - float dy, - float y_min) -{ - int i; - int ii = 0; - int iilast = (int)ny - 1; - float sc = 1 / dy; - int iy0 = (int)floor(sc * (y[ii] - y_min)); - int iy1 = (int)floor(sc * (y[ii + 1] - y_min)); - float invgap = 1.0f / (iy1 - iy0); - for (i = 0; i < nrows && i <= iy0; i++) { - irows[i] = 0; - arows[i] = 1.0; - } - for (; i < nrows; i++) { - while (i > iy1 && ii < iilast) { - ii++; - iy0 = iy1; - iy1 = (int)floor(sc * (y[ii + 1] - y_min)); - invgap = 1.0f / (iy1 - iy0); - } - if (i >= iy0 && i <= iy1) { - irows[i] = ii; - arows[i] = (iy1 - i) * invgap; - } else - break; - } - for (; i < nrows; i++) { - irows[i] = iilast - 1; - arows[i] = 0.0; - } -} - -void _bin_indices(int *irows, int nrows, const double *y, unsigned long ny, double sc, double offs) -{ - int i; - if (sc * (y[ny - 1] - y[0]) > 0) { - int ii = 0; - int iilast = (int)ny - 1; - int iy0 = (int)floor(sc * (y[ii] - offs)); - int iy1 = (int)floor(sc * (y[ii + 1] - offs)); - for (i = 0; i < nrows && i < iy0; i++) { - irows[i] = -1; - } - for (; i < nrows; i++) { - while (i > iy1 && ii < iilast) { - ii++; - iy0 = iy1; - iy1 = (int)floor(sc * (y[ii + 1] - offs)); - } - if (i >= iy0 && i <= iy1) - irows[i] = ii; - else - break; - } - for (; i < nrows; i++) { - irows[i] = -1; - } - } else { - int iilast = (int)ny - 1; - int ii = iilast; - int iy0 = (int)floor(sc * (y[ii] - offs)); - int iy1 = (int)floor(sc * (y[ii - 1] - offs)); - for (i = 0; i < nrows && i < iy0; i++) { - irows[i] = -1; - } - for (; i < nrows; i++) { - while (i > iy1 && ii > 1) { - ii--; - iy0 = iy1; - iy1 = (int)floor(sc * (y[ii - 1] - offs)); - } - if (i >= iy0 && i <= iy1) - irows[i] = ii - 1; - else - break; - } - for (; i < nrows; i++) { - irows[i] = -1; - } - } -} diff --git a/src/_image.h b/src/_image.h deleted file mode 100644 index 37a080fff1d4..000000000000 --- a/src/_image.h +++ /dev/null @@ -1,198 +0,0 @@ -/* -*- mode: c++; c-basic-offset: 4 -*- */ - -/* image.h - * - */ - -#ifndef MPL_IMAGE_H -#define MPL_IMAGE_H - -#include - - -// utilities for irregular grids -void _bin_indices_middle( - unsigned int *irows, int nrows, const float *ys1, unsigned long ny, float dy, float y_min); -void _bin_indices_middle_linear(float *arows, - unsigned int *irows, - int nrows, - const float *y, - unsigned long ny, - float dy, - float y_min); -void _bin_indices(int *irows, int nrows, const double *y, unsigned long ny, double sc, double offs); - -template -void pcolor(CoordinateArray &x, - CoordinateArray &y, - ColorArray &d, - unsigned int rows, - unsigned int cols, - float bounds[4], - int interpolation, - OutputArray &out) -{ - if (rows >= 32768 || cols >= 32768) { - throw std::runtime_error("rows and cols must both be less than 32768"); - } - - float x_min = bounds[0]; - float x_max = bounds[1]; - float y_min = bounds[2]; - float y_max = bounds[3]; - float width = x_max - x_min; - float height = y_max - y_min; - float dx = width / ((float)cols); - float dy = height / ((float)rows); - - // Check we have something to output to - if (rows == 0 || cols == 0) { - throw std::runtime_error("Cannot scale to zero size"); - } - - if (d.dim(2) != 4) { - throw std::runtime_error("data must be in RGBA format"); - } - - // Check dimensions match - unsigned long nx = x.dim(0); - unsigned long ny = y.dim(0); - if (nx != (unsigned long)d.dim(1) || ny != (unsigned long)d.dim(0)) { - throw std::runtime_error("data and axis dimensions do not match"); - } - - // Allocate memory for pointer arrays - std::vector rowstarts(rows); - std::vector colstarts(cols); - - // Calculate the pointer arrays to map input x to output x - unsigned int i, j; - unsigned int *colstart = &colstarts[0]; - unsigned int *rowstart = &rowstarts[0]; - const float *xs1 = x.data(); - const float *ys1 = y.data(); - - // Copy data to output buffer - const unsigned char *start; - const unsigned char *inposition; - size_t inrowsize = nx * 4; - size_t rowsize = cols * 4; - unsigned char *position = (unsigned char *)out.data(); - unsigned char *oldposition = NULL; - start = d.data(); - - if (interpolation == NEAREST) { - _bin_indices_middle(colstart, cols, xs1, nx, dx, x_min); - _bin_indices_middle(rowstart, rows, ys1, ny, dy, y_min); - for (i = 0; i < rows; i++, rowstart++) { - if (i > 0 && *rowstart == 0) { - memcpy(position, oldposition, rowsize * sizeof(unsigned char)); - oldposition = position; - position += rowsize; - } else { - oldposition = position; - start += *rowstart * inrowsize; - inposition = start; - for (j = 0, colstart = &colstarts[0]; j < cols; j++, position += 4, colstart++) { - inposition += *colstart * 4; - memcpy(position, inposition, 4 * sizeof(unsigned char)); - } - } - } - } else if (interpolation == BILINEAR) { - std::vector acols(cols); - std::vector arows(rows); - - _bin_indices_middle_linear(&acols[0], colstart, cols, xs1, nx, dx, x_min); - _bin_indices_middle_linear(&arows[0], rowstart, rows, ys1, ny, dy, y_min); - double a00, a01, a10, a11, alpha, beta; - - // Copy data to output buffer - for (i = 0; i < rows; i++) { - for (j = 0; j < cols; j++) { - alpha = arows[i]; - beta = acols[j]; - - a00 = alpha * beta; - a01 = alpha * (1.0 - beta); - a10 = (1.0 - alpha) * beta; - a11 = 1.0 - a00 - a01 - a10; - - for (size_t k = 0; k < 4; ++k) { - position[k] = - d(rowstart[i], colstart[j], k) * a00 + - d(rowstart[i], colstart[j] + 1, k) * a01 + - d(rowstart[i] + 1, colstart[j], k) * a10 + - d(rowstart[i] + 1, colstart[j] + 1, k) * a11; - } - position += 4; - } - } - } -} - -template -void pcolor2(CoordinateArray &x, - CoordinateArray &y, - ColorArray &d, - unsigned int rows, - unsigned int cols, - float bounds[4], - Color &bg, - OutputArray &out) -{ - double x_left = bounds[0]; - double x_right = bounds[1]; - double y_bot = bounds[2]; - double y_top = bounds[3]; - - // Check we have something to output to - if (rows == 0 || cols == 0) { - throw std::runtime_error("rows or cols is zero; there are no pixels"); - } - - if (d.dim(2) != 4) { - throw std::runtime_error("data must be in RGBA format"); - } - - // Check dimensions match - unsigned long nx = x.dim(0); - unsigned long ny = y.dim(0); - if (nx != (unsigned long)d.dim(1) + 1 || ny != (unsigned long)d.dim(0) + 1) { - throw std::runtime_error("data and axis bin boundary dimensions are incompatible"); - } - - if (bg.dim(0) != 4) { - throw std::runtime_error("bg must be in RGBA format"); - } - - std::vector irows(rows); - std::vector jcols(cols); - - // Calculate the pointer arrays to map input x to output x - size_t i, j; - const double *x0 = x.data(); - const double *y0 = y.data(); - double sx = cols / (x_right - x_left); - double sy = rows / (y_top - y_bot); - _bin_indices(&jcols[0], cols, x0, nx, sx, x_left); - _bin_indices(&irows[0], rows, y0, ny, sy, y_bot); - - // Copy data to output buffer - unsigned char *position = (unsigned char *)out.data(); - - for (i = 0; i < rows; i++) { - for (j = 0; j < cols; j++) { - if (irows[i] == -1 || jcols[j] == -1) { - memcpy(position, (const unsigned char *)bg.data(), 4 * sizeof(unsigned char)); - } else { - for (size_t k = 0; k < 4; ++k) { - position[k] = d(irows[i], jcols[j], k); - } - } - position += 4; - } - } -} - -#endif diff --git a/src/_image_wrapper.cpp b/src/_image_wrapper.cpp index 73d093aa3b18..a15e67c373e8 100644 --- a/src/_image_wrapper.cpp +++ b/src/_image_wrapper.cpp @@ -1,6 +1,5 @@ #include "mplutils.h" #include "_image_resample.h" -#include "_image.h" #include "numpy_cpp.h" #include "py_converters.h" @@ -290,98 +289,8 @@ image_resample(PyObject *self, PyObject* args, PyObject *kwargs) return NULL; } - -const char *image_pcolor__doc__ = - "pcolor(x, y, data, rows, cols, bounds)\n" - "\n" - "Generate a pseudo-color image from data on a non-uniform grid using\n" - "nearest neighbour or linear interpolation.\n" - "bounds = (x_min, x_max, y_min, y_max)\n" - "interpolation = NEAREST or BILINEAR \n"; - -static PyObject *image_pcolor(PyObject *self, PyObject *args, PyObject *kwds) -{ - numpy::array_view x; - numpy::array_view y; - numpy::array_view d; - npy_intp rows, cols; - float bounds[4]; - int interpolation; - - if (!PyArg_ParseTuple(args, - "O&O&O&nn(ffff)i:pcolor", - &x.converter, - &x, - &y.converter, - &y, - &d.converter_contiguous, - &d, - &rows, - &cols, - &bounds[0], - &bounds[1], - &bounds[2], - &bounds[3], - &interpolation)) { - return NULL; - } - - npy_intp dim[3] = {rows, cols, 4}; - numpy::array_view output(dim); - - CALL_CPP("pcolor", (pcolor(x, y, d, rows, cols, bounds, interpolation, output))); - - return output.pyobj(); -} - -const char *image_pcolor2__doc__ = - "pcolor2(x, y, data, rows, cols, bounds, bg)\n" - "\n" - "Generate a pseudo-color image from data on a non-uniform grid\n" - "specified by its cell boundaries.\n" - "bounds = (x_left, x_right, y_bot, y_top)\n" - "bg = ndarray of 4 uint8 representing background rgba\n"; - -static PyObject *image_pcolor2(PyObject *self, PyObject *args, PyObject *kwds) -{ - numpy::array_view x; - numpy::array_view y; - numpy::array_view d; - npy_intp rows, cols; - float bounds[4]; - numpy::array_view bg; - - if (!PyArg_ParseTuple(args, - "O&O&O&nn(ffff)O&:pcolor2", - &x.converter_contiguous, - &x, - &y.converter_contiguous, - &y, - &d.converter_contiguous, - &d, - &rows, - &cols, - &bounds[0], - &bounds[1], - &bounds[2], - &bounds[3], - &bg.converter, - &bg)) { - return NULL; - } - - npy_intp dim[3] = {rows, cols, 4}; - numpy::array_view output(dim); - - CALL_CPP("pcolor2", (pcolor2(x, y, d, rows, cols, bounds, bg, output))); - - return output.pyobj(); -} - static PyMethodDef module_functions[] = { {"resample", (PyCFunction)image_resample, METH_VARARGS|METH_KEYWORDS, image_resample__doc__}, - {"pcolor", (PyCFunction)image_pcolor, METH_VARARGS, image_pcolor__doc__}, - {"pcolor2", (PyCFunction)image_pcolor2, METH_VARARGS, image_pcolor2__doc__}, {NULL} };