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

Skip to content

Commit 4c931ad

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 4c931ad

File tree

5 files changed

+66
-431
lines changed

5 files changed

+66
-431
lines changed

lib/matplotlib/image.py

Lines changed: 66 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,14 +1052,49 @@ 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+
(x_pix - self._Ax[x_int]) / np.diff(self._Ax)[x_int], 0, 1,
1080+
dtype=np.float32) # Downcasting helps with speed.
1081+
y_frac = np.clip(
1082+
(y_pix - self._Ay[y_int]) / np.diff(self._Ay)[y_int], 0, 1,
1083+
dtype=np.float32)
1084+
f00 = np.outer(1 - y_frac, 1 - x_frac)
1085+
f10 = np.outer(y_frac, 1 - x_frac)
1086+
f01 = np.outer(1 - y_frac, x_frac)
1087+
f11 = np.outer(y_frac, x_frac)
1088+
im = np.empty((height, width, 4), np.uint8)
1089+
for chan in range(4):
1090+
ac = A[:, :, chan].reshape(-1) # reshape(-1) avoids a copy.
1091+
# Shifting the buffer start (`ac[offset:]`) avoids an array
1092+
# addition (`ac[idx_int + offset]`).
1093+
buf = f00 * ac[idx_int]
1094+
buf += f10 * ac[A.shape[1]:][idx_int]
1095+
buf += f01 * ac[1:][idx_int]
1096+
buf += f11 * ac[A.shape[1] + 1:][idx_int]
1097+
im[:, :, chan] = buf # Implicitly casts to uint8.
10631098
return im, l, b, IdentityTransform()
10641099

10651100
def set_data(self, x, y, A):
@@ -1183,27 +1218,35 @@ def make_image(self, renderer, magnification=1.0, unsampled=False):
11831218
raise RuntimeError('You must first set the image array')
11841219
if unsampled:
11851220
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))
1221+
11941222
if self._rgbacache is None:
11951223
A = self.to_rgba(self._A, bytes=True)
1196-
self._rgbacache = A
1224+
padded_A = np.pad(A, [(1, 1), (1, 1), (0, 0)])
1225+
self._rgbacache = padded_A
11971226
if self._A.ndim == 2:
11981227
self._is_grayscale = self.cmap.is_gray()
11991228
else:
1200-
A = self._rgbacache
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+
1235+
l, b, r, t = self.axes.bbox.extents
1236+
width = (round(r) + 0.5) - (round(l) - 0.5)
1237+
height = (round(t) + 0.5) - (round(b) - 0.5)
1238+
width = int(round(width * magnification))
1239+
height = int(round(height * magnification))
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):

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