Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 3da58a1

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 3da58a1

File tree

7 files changed

+82
-431
lines changed

7 files changed

+82
-431
lines changed

lib/matplotlib/image.py

Lines changed: 66 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,33 @@ 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)
1223+
1224+
if self._rgbacache is None:
1225+
A = self.to_rgba(self._A, bytes=True)
1226+
self._rgbacache = np.pad(A, [(1, 1), (1, 1), (0, 0)], "constant")
1227+
if self._A.ndim == 2:
1228+
self._is_grayscale = self.cmap.is_gray()
1229+
padded_A = self._rgbacache
1230+
bg = mcolors.to_rgba(self.axes.patch.get_facecolor(), 0)
1231+
bg = (np.array(bg) * 255).astype(np.uint8)
1232+
if (padded_A[0, 0] != bg).all():
1233+
padded_A[[0, -1], :] = padded_A[:, [0, -1]] = bg
1234+
11891235
l, b, r, t = self.axes.bbox.extents
11901236
width = (round(r) + 0.5) - (round(l) - 0.5)
11911237
height = (round(t) + 0.5) - (round(b) - 0.5)
11921238
width = int(round(width * magnification))
11931239
height = int(round(height * magnification))
1194-
if self._rgbacache is None:
1195-
A = self.to_rgba(self._A, bytes=True)
1196-
self._rgbacache = A
1197-
if self._A.ndim == 2:
1198-
self._is_grayscale = self.cmap.is_gray()
1199-
else:
1200-
A = self._rgbacache
12011240
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)
1241+
1242+
x_pix = np.linspace(vl.x0, vl.x1, width)
1243+
y_pix = np.linspace(vl.y0, vl.y1, height)
1244+
x_int = self._Ax.searchsorted(x_pix)
1245+
y_int = self._Ay.searchsorted(y_pix)
1246+
im = ( # See comment in NonUniformImage.make_image re: performance.
1247+
padded_A.view(np.uint32).ravel()[
1248+
np.add.outer(y_int * padded_A.shape[1], x_int)]
1249+
.view(np.uint8).reshape((height, width, 4)))
12071250
return im, l, b, IdentityTransform()
12081251

12091252
def _check_unsampled_image(self):

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)