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

Skip to content

Commit 177e846

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 177e846

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):
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.add_image(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