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

Skip to content

Commit ea2de97

Browse files
committed
Reimplement NonUniformImage, PcolorImage in Python, not C.
It's much shorter... 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 303873f commit ea2de97

File tree

8 files changed

+91
-431
lines changed

8 files changed

+91
-431
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
The output of ``NonUniformImage`` and ``PcolorImage`` has changed
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
Pixel-level differences may be observed in images generated using
4+
`.NonUniformImage` or `.PcolorImage`, typically for pixels exactly at the
5+
boundary between two data cells (no user-facing axes method currently generates
6+
`.NonUniformImage`\s, and only `.pcolorfast` can generate `.PcolorImage`\s).
7+
These artists are also now slower, normally by ~1.5x but sometimes more (in
8+
particular for ``NonUniformImage(interpolation="bilinear")``. This slowdown
9+
arises from fixing occasional floating point inaccuracies.

lib/matplotlib/image.py

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

10681105
def set_data(self, x, y, A):
@@ -1186,27 +1223,33 @@ def make_image(self, renderer, magnification=1.0, unsampled=False):
11861223
raise RuntimeError('You must first set the image array')
11871224
if unsampled:
11881225
raise ValueError('unsampled not supported on PColorImage')
1189-
fc = self.axes.patch.get_facecolor()
1190-
bg = mcolors.to_rgba(fc, 0)
1191-
bg = (np.array(bg)*255).astype(np.uint8)
1226+
1227+
if self._rgbacache is None:
1228+
A = self.to_rgba(self._A, bytes=True)
1229+
self._rgbacache = np.pad(A, [(1, 1), (1, 1), (0, 0)], "constant")
1230+
if self._A.ndim == 2:
1231+
self._is_grayscale = self.cmap.is_gray()
1232+
padded_A = self._rgbacache
1233+
bg = mcolors.to_rgba(self.axes.patch.get_facecolor(), 0)
1234+
bg = (np.array(bg) * 255).astype(np.uint8)
1235+
if (padded_A[0, 0] != bg).all():
1236+
padded_A[[0, -1], :] = padded_A[:, [0, -1]] = bg
1237+
11921238
l, b, r, t = self.axes.bbox.extents
11931239
width = (round(r) + 0.5) - (round(l) - 0.5)
11941240
height = (round(t) + 0.5) - (round(b) - 0.5)
11951241
width = int(round(width * magnification))
11961242
height = int(round(height * magnification))
1197-
if self._rgbacache is None:
1198-
A = self.to_rgba(self._A, bytes=True)
1199-
self._rgbacache = A
1200-
if self._A.ndim == 2:
1201-
self._is_grayscale = self.cmap.is_gray()
1202-
else:
1203-
A = self._rgbacache
12041243
vl = self.axes.viewLim
1205-
im = _image.pcolor2(self._Ax, self._Ay, A,
1206-
height,
1207-
width,
1208-
(vl.x0, vl.x1, vl.y0, vl.y1),
1209-
bg)
1244+
1245+
x_pix = np.linspace(vl.x0, vl.x1, width)
1246+
y_pix = np.linspace(vl.y0, vl.y1, height)
1247+
x_int = self._Ax.searchsorted(x_pix)
1248+
y_int = self._Ay.searchsorted(y_pix)
1249+
im = ( # See comment in NonUniformImage.make_image re: performance.
1250+
padded_A.view(np.uint32).ravel()[
1251+
np.add.outer(y_int * padded_A.shape[1], x_int)]
1252+
.view(np.uint8).reshape((height, width, 4)))
12101253
return im, l, b, IdentityTransform()
12111254

12121255
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
@@ -1263,3 +1263,19 @@ def test_spy_box(fig_test, fig_ref):
12631263
ax_ref[i].yaxis.set_major_locator(
12641264
mticker.MaxNLocator(nbins=9, steps=[1, 2, 5, 10], integer=True)
12651265
)
1266+
1267+
1268+
@image_comparison(["nonuniform_and_pcolor.png"], style="mpl20")
1269+
def test_nonuniform_and_pcolor():
1270+
axs = plt.figure(figsize=(3, 3)).subplots(3, sharex=True, sharey=True)
1271+
for ax, interpolation in zip(axs, ["nearest", "bilinear"]):
1272+
im = NonUniformImage(ax, interpolation=interpolation)
1273+
im.set_data(np.arange(3) ** 2, np.arange(3) ** 2,
1274+
np.arange(9).reshape((3, 3)))
1275+
ax.add_image(im)
1276+
axs[2].pcolorfast( # PcolorImage
1277+
np.arange(4) ** 2, np.arange(4) ** 2, np.arange(9).reshape((3, 3)))
1278+
for ax in axs:
1279+
ax.set_axis_off()
1280+
# NonUniformImage "leaks" out of extents, not PColorImage.
1281+
ax.set(xlim=(0, 10))

setupext.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,6 @@ def get_extensions(self):
413413
# image
414414
ext = Extension(
415415
"matplotlib._image", [
416-
"src/_image.cpp",
417416
"src/mplutils.cpp",
418417
"src/_image_wrapper.cpp",
419418
"src/py_converters.cpp",

src/_image.cpp

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

0 commit comments

Comments
 (0)