8000 Reimplement NonUniformImage, PcolorImage in Python, not C. · matplotlib/matplotlib@f89e5ad · GitHub
[go: up one dir, main page]

Skip to content

Commit f89e5ad

Browse files
committed
Reimplement NonUniformImage, PcolorImage in Python, not C.
It's much shorter... None of this has test coverage though :( -- probably needed for the PR; but one can first check that `examples/images_contours_and_fields/image_nonuniform.py` still works. Perf check: ```python from timeit import Timer from matplotlib import pyplot as plt from matplotlib.image import NonUniformImage, PcolorImage import numpy as np N = 100 fig, (ax_nn, ax_nb, ax_pc) = plt.subplots(3) ax_nn.set(xlim=(-.5, .75), ylim=(-.5, .75)) nn = NonUniformImage(ax_nn) nn.set_data(np.linspace(0, 1, 2 * N) ** 2, np.linspace(0, 1, N) ** 2, np.arange(2 * N**2).reshape((N, 2 * N))) ax_nn.images.append(nn) ax_nb.set(xlim=(-.5, .75), ylim=(-.5, .75)) nb = NonUniformImage(ax_nb, interpolation="bilinear") nb.set_data(np.linspace(0, 1, 2 * N) ** 2, np.linspace(0, 1, N) ** 2, np.arange(2 * N**2).reshape((N, 2 * N))) ax_nb.images.append(nb) ax_pc.set(xlim=(-.5, .75), ylim=(-.5, .75)) pc = PcolorImage(ax_pc) pc.set_data(np.linspace(0, 1, 2 * N + 1) ** 2, np.linspace(0, 1, N + 1) ** 2, np.arange(2 * N**2).reshape((N, 2 * N))) ax_pc.images.append(pc) fig.canvas.draw() n, t = Timer("nn.make_image(fig._cachedRenderer)", globals=globals()).autorange() print(f"NN: {1000*t/n:.4f}ms") n, t = Timer("nb.make_image(fig._cachedRenderer)", globals=globals()).autorange() print(f"NB: {1000*t/n:.4f}ms") n, t = Timer("pc.make_image(fig._cachedRenderer)", globals=globals()).autorange() print(f"PC: {1000*t/n:.4f}ms") plt.show() ```
1 parent 3b1be53 commit f89e5ad

File tree

7 files changed

+84
-431
lines changed
  • src
  • 7 files changed

    +84
    -431
    lines changed

    lib/matplotlib/image.py

    Lines changed: 68 additions & 23 deletions
    Original file line numberDiff line numberDiff line change
    @@ -1052,14 +1052,51 @@ def make_image(self, renderer, magnification=1.0, unsampled=False):
    10521052
    self._is_grayscale = False
    10531053
    vl = self.axes.viewLim
    10541054
    l, b, r, t = self.axes.bbox.extents
    1055-
    width = (round(r) + 0.5) - (round(l) - 0.5)
    1056-
    height = (round(t) + 0.5) - (round(b) - 0.5)
    1057-
    width *= magnification
    1058-
    height *= magnification
    1059-
    im = _image.pcolor(self._Ax, self._Ay, A,
    1060-
    int(height), int(width),
    1061-
    (vl.x0, vl.x1, vl.y0, vl.y1),
    1062-
    _interpd_[self._interpolation])
    1055+
    width = int(((round(r) + 0.5) - (round(l) - 0.5)) * magnification)
    1056+
    height = int(((round(t) + 0.5) - (round(b) - 0.5)) * magnification)
    1057+
    x_pix = np.linspace(vl.x0, vl.x1, width)
    1058+
    y_pix = np.linspace(vl.y0, vl.y1, height)
    1059+
    if self._interpolation == "nearest":
    1060+
    x_mid = (self._Ax[:-1] + self._Ax[1:]) / 2
    1061+
    y_mid = (self._Ay[:-1] + self._Ay[1:]) / 2
    1062+
    x_int = x_mid.searchsorted(x_pix)
    1063+
    y_int = y_mid.searchsorted(y_pix)
    1064+
    # The following is equal to `A[y_int[:, None], x_int[None, :]]`,
    1065+
    # but many times faster. Both casting to uint32 (to have an
    1066+
    # effectively 1D array) and manual index flattening matter.
    1067+
    im = (
    1068+
    np.ascontiguousarray(A).view(np.uint32).ravel()[
    1069+
    np.add.outer(y_int * A.shape[1], x_int)]
    1070+
    .view(np.uint8).reshape((height, width, 4)))
    1071+
    else: # self._interpolation == "bilinear"
    1072+
    # Use np.interp to compute x_int/x_float has similar speed.
    1073+
    x_int = np.clip(
    1074+
    self._Ax.searchsorted(x_pix) - 1, 0, len(self._Ax) - 2)
    1075+
    y_int = np.clip(
    1076+
    self._Ay.searchsorted(y_pix) - 1, 0, len(self._Ay) - 2)
    1077+
    idx_int = np.add.outer(y_int * A.shape[1], x_int)
    1078+
    x_frac = np.clip(
    1079+
    np.divide(x_pix - self._Ax[x_int], np.diff(self._Ax)[x_int],
    1080+
    dtype=np.float32), # Downcasting helps with speed.
    1081+
    0, 1)
    1082+
    y_frac = np.clip(
    1083+
    np.divide(y_pix - self._Ay[y_int], np.diff(self._Ay)[y_int],
    1084+
    dtype=np.float32),
    1085+
    0, 1)
    1086+
    f00 = np.outer(1 - y_frac, 1 - x_frac)
    1087+
    f10 = np.outer(y_frac, 1 - x_frac)
    1088+
    f01 = np.outer(1 - y_frac, x_frac)
    1089+
    f11 = np.outer(y_frac, x_frac)
    1090+
    im = np.empty((height, width, 4), np.uint8)
    1091+
    for chan in range(4):
    1092+
    ac = A[:, :, chan].reshape(-1) # reshape(-1) avoids a copy.
    1093+
    # Shifting the buffer start (`ac[offset:]`) avoids an array
    1094+
    # addition (`ac[idx_int + offset]`).
    1095+
    buf = f00 * ac[idx_int]
    1096+
    buf += f10 * ac[A.shape[1]:][idx_int]
    1097+
    buf += f01 * ac[1:][idx_int]
    1098+
    buf += f11 * ac[A.shape[1] + 1:][idx_int]
    1099+
    im[:, :, chan] = buf # Implicitly casts to uint8.
    10631100
    return im, l, b, IdentityTransform()
    10641101

    10651102
    def set_data(self, x, y, A):
    @@ -1183,27 +1220,35 @@ def make_image(self, renderer, magnification=1.0, unsampled=False):
    11831220
    raise RuntimeError('You must first set the image array')
    11841221
    if unsampled:
    11851222
    raise ValueError('unsampled not supported on PColorImage')
    1186-
    fc = self.axes.patch.get_facecolor()
    1187-
    bg = mcolors.to_rgba(fc, 0)
    1188-
    bg = (np.array(bg)*255).astype(np.uint8)
    1189-
    l, b, r, t = self.axes.bbox.extents
    1190-
    width = (round(r) + 0.5) - (round(l) - 0.5)
    1191-
    height = (round(t) + 0.5) - (round(b) - 0.5)
    1192-
    width = int(round(width * magnification))
    1193-
    height = int(round(height * magnification))
    1223+
    11941224
    if self._rgbacache is None:
    11951225
    A = self.to_rgba(self._A, bytes=True)
    1196-
    self._rgbacache = A
    1226+
    padded_A = np.pad(A, [(1, 1), (1, 1), (0, 0)])
    1227+
    self._rgbacache = padded_A
    11971228
    if self._A.ndim == 2:
    11981229
    self._is_grayscale = self.cmap.is_gray()
    11991230
    else:
    1200-
    A = self._rgbacache
    1231+
    padded_A = self._rgbacache
    1232+
    bg = mcolors.to_rgba(self.axes.patch.get_facecolor(), 0)
    1233+
    bg = (np.array(bg) * 255).astype(np.uint8)
    1234+
    if (padded_A[0, 0] != bg).all():
    1235+
    padded_A[[0, -1], :] = padded_A[:, [0, -1]] = bg
    1236+
    1237+
    l, b, r, t = self.axes.bbox.extents
    1238+
    width = (round(r) + 0.5) - (round(l) - 0.5)
    1239+
    height = (round(t) + 0.5) - (round(b) - 0.5)
    1240+
    width = int(round(width * magnification))
    1241+
    height = int(round(height * magnification))
    12011242
    vl = self.axes.viewLim
    1202-
    im = _image.pcolor2(self._Ax, self._Ay, A,
    1203-
    height,
    1204-
    width,
    1205-
    (vl.x0, vl.x1, vl.y0, vl.y1),
    1206-
    bg)
    1243+
    1244+
    x_pix = np.linspace(vl.x0, vl.x1, width)
    1245+
    y_pix = np.linspace(vl.y0, vl.y1, height)
    1246+
    x_int = self._Ax.searchsorted(x_pix)
    1247+
    y_int = self._Ay.searchsorted(y_pix)
    1248+
    im = ( # See comment in NonUniformImage.make_image re: performance.
    1249+
    padded_A.view(np.uint32).ravel()[
    1250+
    np.add.outer(y_int * padded_A.shape[1], x_int)]
    1251+
    .view(np.uint8).reshape((height, width, 4)))
    12071252
    return im, l, b, IdentityTransform()
    12081253

    12091254
    def _check_unsampled_image(self):
    Loading

    lib/matplotlib/tests/test_image.py

    Lines changed: 16 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -1224,3 +1224,19 @@ def test_huge_range_log(fig_test, fig_ref):
    12241224
    ax = fig_ref.subplots()
    12251225
    im = ax.imshow(data, norm=colors.Normalize(vmin=100, vmax=data.max()),
    12261226
    interpolation='nearest', cmap=cm)
    1227+
    1228+
    1229+
    @image_comparison(["nonuniform_and_pcolor.png"], style="mpl20")
    1230+
    def test_nonuniform_and_pcolor():
    1231+
    axs = plt.figure(figsize=(3, 3)).subplots(3, sharex=True, sharey=True)
    1232+
    for ax, interpolation in zip(axs, ["nearest", "bilinear"]):
    1233+
    im = NonUniformImage(ax, interpolation=interpolation)
    1234+
    im.set_data(np.arange(3) ** 2, np.arange(3) ** 2,
    1235+
    np.arange(9).reshape((3, 3)))
    1236+
    ax.images.append(im)
    1237+
    axs[2].pcolorfast( # PcolorImage
    1238+
    np.arange(4) ** 2, np.arange(4) ** 2, np.arange(9).reshape((3, 3)))
    1239+
    for ax in axs:
    1240+
    ax.set_axis_off()
    1241+
    # NonUniformImage "leaks" out of extents, not PColorImage.
    1242+
    ax.set(xlim=(0, 10))

    setupext.py

    Lines changed: 0 additions & 1 deletion
    Original file line numberDiff line numberDiff line change
    @@ -377,7 +377,6 @@ def get_extensions(self):
    377377
    # image
    378378
    ext = Extension(
    379379
    "matplotlib._image", [
    380-
    "src/_image.cpp",
    381380
    "src/mplutils.cpp",
    382381
    "src/_image_wrapper.cpp",
    383382
    "src/py_converters.cpp",

    src/_image.cpp

    Lines changed: 0 additions & 118 deletions
    This file was deleted.

    0 commit comments

    Comments
     (0)
    0