Skip to content

Commit f044270

Browse files
authored
Merge pull request #14913 from anntzer/pcolor
Reimplement NonUniformImage, PcolorImage in Python, not C.
2 parents b76d399 + ea2de97 commit f044270

File tree

8 files changed

+91
-431
lines changed

8 files changed

+91
-431
lines changed
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

+66-23
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

+16
Original file line numberDiff line numberDiff line change
@@ -1276,3 +1276,19 @@ def test_spy_box(fig_test, fig_ref):
12761276
ax_ref[i].yaxis.set_major_locator(
12771277
mticker.MaxNLocator(nbins=9, steps=[1, 2, 5, 10], integer=True)
12781278
)
1279+
1280+
1281+
@image_comparison(["nonuniform_and_pcolor.png"], style="mpl20")
1282+
def test_nonuniform_and_pcolor():
1283+
axs = plt.figure(figsize=(3, 3)).subplots(3, sharex=True, sharey=True)
1284+
for ax, interpolation in zip(axs, ["nearest", "bilinear"]):
1285+
im = NonUniformImage(ax, interpolation=interpolation)
1286+
im.set_data(np.arange(3) ** 2, np.arange(3) ** 2,
1287+
np.arange(9).reshape((3, 3)))
1288+
ax.add_image(im)
1289+
axs[2].pcolorfast( # PcolorImage
1290+
np.arange(4) ** 2, np.arange(4) ** 2, np.arange(9).reshape((3, 3)))
1291+
for ax in axs:
1292+
ax.set_axis_off()
1293+
# NonUniformImage "leaks" out of extents, not PColorImage.
1294+
ax.set(xlim=(0, 10))

setupext.py

-1
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

-118
This file was deleted.

0 commit comments

Comments
 (0)