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

Skip to content

Commit 86f65d7

Browse files
authored
Merge pull request matplotlib#31021 from ayshih/make_image_fix
Fixed inaccurate image placement and even more resampling bugs
2 parents 47874b4 + 800185c commit 86f65d7

89 files changed

Lines changed: 965 additions & 675 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Rendering of images now more accurate
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
There have been several fixes to improve the accuracy of how images are
4+
resampled and placed during rendering. Some inaccuracies were up to a pixel off
5+
in the output. The most apparent improvement is that the alignment of data
6+
pixels with tick marks and grid lines is now reliable. Nearly all image output
7+
has changed, but often only at a subtle level that is not obvious qualitatively.

lib/matplotlib/image.py

Lines changed: 56 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
operations.
44
"""
55

6-
import math
76
import os
87
import logging
98
from pathlib import Path
@@ -207,12 +206,24 @@ def _resample(
207206
out = np.zeros(out_shape + data.shape[2:], data.dtype) # 2D->2D, 3D->3D.
208207
if resample is None:
209208
resample = image_obj.get_resample()
209+
210+
# When an output pixel falls exactly on the edge between two input pixels, the Agg
211+
# resampler will use the right input pixel as the nearest neighbor. We want the
212+
# left input pixel to be chosen instead, so we flip the supplied transform.
213+
if interpolation == 'nearest':
214+
transform += Affine2D().translate(-out.shape[1], -out.shape[0]).scale(-1, -1)
215+
210216
_image.resample(data, out, transform,
211217
_interpd_[interpolation],
212218
resample,
213219
alpha,
214220
image_obj.get_filternorm(),
215221
image_obj.get_filterrad())
222+
223+
# Because we flipped the supplied transform, we then flip the output image back.
224+
if interpolation == 'nearest':
225+
out = np.flip(out, axis=(0, 1))
226+
216227
return out
217228

218229

@@ -393,10 +404,15 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
393404
if clipped_bbox is None:
394405
return None, 0, 0, None
395406

396-
out_width_base = clipped_bbox.width * magnification
397-
out_height_base = clipped_bbox.height * magnification
407+
# Define the magnified bbox after clipping
408+
magnified_extents = clipped_bbox.extents * magnification
409+
if ((not unsampled) and round_to_pixel_border):
410+
# Round to the nearest output pixel
411+
magnified_bbox = Bbox.from_extents((magnified_extents + 0.5).astype(int))
412+
else:
413+
magnified_bbox = Bbox.from_extents(magnified_extents)
398414

399-
if out_width_base == 0 or out_height_base == 0:
415+
if magnified_bbox.width == 0 or magnified_bbox.height == 0:
400416
return None, 0, 0, None
401417

402418
if self.origin == 'upper':
@@ -417,23 +433,10 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
417433

418434
t = (t0
419435
+ (Affine2D()
420-
.translate(-clipped_bbox.x0, -clipped_bbox.y0)
421-
.scale(magnification)))
422-
423-
# So that the image is aligned with the edge of the Axes, we want to
424-
# round up the output width to the next integer. This also means
425-
# scaling the transform slightly to account for the extra subpixel.
426-
if ((not unsampled) and t.is_affine and round_to_pixel_border and
427-
(out_width_base % 1.0 != 0.0 or out_height_base % 1.0 != 0.0)):
428-
out_width = math.ceil(out_width_base)
429-
out_height = math.ceil(out_height_base)
430-
extra_width = (out_width - out_width_base) / out_width_base
431-
extra_height = (out_height - out_height_base) / out_height_base
432-
t += Affine2D().scale(1.0 + extra_width, 1.0 + extra_height)
433-
else:
434-
out_width = int(out_width_base)
435-
out_height = int(out_height_base)
436-
out_shape = (out_height, out_width)
436+
.scale(magnification)
437+
.translate(-magnified_bbox.x0, -magnified_bbox.y0)))
438+
439+
out_shape = (int(magnified_bbox.height), int(magnified_bbox.width))
437440

438441
if not unsampled:
439442
if not (A.ndim == 2 or A.ndim == 3 and A.shape[-1] in (3, 4)):
@@ -560,7 +563,10 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
560563
t = Affine2D().translate(
561564
int(max(subset.xmin, 0)), int(max(subset.ymin, 0))) + t
562565

563-
return output, clipped_bbox.x0, clipped_bbox.y0, t
566+
return (output,
567+
magnified_bbox.x0 / magnification,
568+
magnified_bbox.y0 / magnification,
569+
t)
564570

565571
def make_image(self, renderer, magnification=1.0, unsampled=False):
566572
"""
@@ -1061,21 +1067,24 @@ def make_image(self, renderer, magnification=1.0, unsampled=False):
10611067
B[:, :, 0:3] = A
10621068
B[:, :, 3] = 255
10631069
A = B
1064-
l, b, r, t = self.axes.bbox.extents
1065-
width = int(((round(r) + 0.5) - (round(l) - 0.5)) * magnification)
1066-
height = int(((round(t) + 0.5) - (round(b) - 0.5)) * magnification)
1070+
magnified_extents = (self.axes.bbox.extents * magnification + 0.5).astype(int)
1071+
l, b, r, t = magnified_extents / magnification
1072+
width = int((r - l) * magnification)
1073+
height = int((t - b) * magnification)
10671074

10681075
invertedTransform = self.axes.transData.inverted()
1069-
x_pix = invertedTransform.transform(
1070-
[(x, b) for x in np.linspace(l, r, width)])[:, 0]
1071-
y_pix = invertedTransform.transform(
1072-
[(l, y) for y in np.linspace(b, t, height)])[:, 1]
1076+
x_pix_edges = invertedTransform.transform(
1077+
[(x, b) for x in np.linspace(l, r, width + 1)])[:, 0]
1078+
y_pix_edges = invertedTransform.transform(
1079+
[(l, y) for y in np.linspace(b, t, height + 1)])[:, 1]
1080+
x_pix_centers = (x_pix_edges[:-1] + x_pix_edges[1:]) / 2
1081+
y_pix_centers = (y_pix_edges[:-1] + y_pix_edges[1:]) / 2
10731082

10741083
if self._interpolation == "nearest":
10751084
x_mid = (self._Ax[:-1] + self._Ax[1:]) / 2
10761085
y_mid = (self._Ay[:-1] + self._Ay[1:]) / 2
1077-
x_int = x_mid.searchsorted(x_pix)
1078-
y_int = y_mid.searchsorted(y_pix)
1086+
x_int = x_mid.searchsorted(x_pix_centers)
1087+
y_int = y_mid.searchsorted(y_pix_centers)
10791088
# The following is equal to `A[y_int[:, None], x_int[None, :]]`,
10801089
# but many times faster. Both casting to uint32 (to have an
10811090
# effectively 1D array) and manual index flattening matter.
@@ -1086,16 +1095,16 @@ def make_image(self, renderer, magnification=1.0, unsampled=False):
10861095
else: # self._interpolation == "bilinear"
10871096
# Use np.interp to compute x_int/x_float has similar speed.
10881097
x_int = np.clip(
1089-
self._Ax.searchsorted(x_pix) - 1, 0, len(self._Ax) - 2)
1098+
self._Ax.searchsorted(x_pix_centers) - 1, 0, len(self._Ax) - 2)
10901099
y_int = np.clip(
1091-
self._Ay.searchsorted(y_pix) - 1, 0, len(self._Ay) - 2)
1100+
self._Ay.searchsorted(y_pix_centers) - 1, 0, len(self._Ay) - 2)
10921101
idx_int = np.add.outer(y_int * A.shape[1], x_int)
10931102
x_frac = np.clip(
1094-
np.divide(x_pix - self._Ax[x_int], np.diff(self._Ax)[x_int],
1103+
np.divide(x_pix_centers - self._Ax[x_int], np.diff(self._Ax)[x_int],
10951104
dtype=np.float32), # Downcasting helps with speed.
10961105
0, 1)
10971106
y_frac = np.clip(
1098-
np.divide(y_pix - self._Ay[y_int], np.diff(self._Ay)[y_int],
1107+
np.divide(y_pix_centers - self._Ay[y_int], np.diff(self._Ay)[y_int],
10991108
dtype=np.float32),
11001109
0, 1)
11011110
f00 = np.outer(1 - y_frac, 1 - x_frac)
@@ -1248,22 +1257,24 @@ def make_image(self, renderer, magnification=1.0, unsampled=False):
12481257
if (padded_A[0, 0] != bg).all():
12491258
padded_A[[0, -1], :] = padded_A[:, [0, -1]] = bg
12501259

1251-
l, b, r, t = self.axes.bbox.extents
1252-
width = (round(r) + 0.5) - (round(l) - 0.5)
1253-
height = (round(t) + 0.5) - (round(b) - 0.5)
1254-
width = round(width * magnification)
1255-
height = round(height * magnification)
1260+
# Round to the nearest output pixels after magnification
1261+
l, b, r, t = (self.axes.bbox.extents * magnification + 0.5).astype(int)
1262+
width = r - l
1263+
height = t - b
1264+
12561265
vl = self.axes.viewLim
12571266

1258-
x_pix = np.linspace(vl.x0, vl.x1, width)
1259-
y_pix = np.linspace(vl.y0, vl.y1, height)
1260-
x_int = self._Ax.searchsorted(x_pix)
1261-
y_int = self._Ay.searchsorted(y_pix)
1267+
x_pix_edges = np.linspace(vl.x0, vl.x1, width + 1)
1268+
y_pix_edges = np.linspace(vl.y0, vl.y1, height + 1)
1269+
x_pix_centers = (x_pix_edges[:-1] + x_pix_edges[1:]) / 2
1270+
y_pix_centers = (y_pix_edges[:-1] + y_pix_edges[1:]) / 2
1271+
x_int = self._Ax.searchsorted(x_pix_centers)
1272+
y_int = self._Ay.searchsorted(y_pix_centers)
12621273
im = ( # See comment in NonUniformImage.make_image re: performance.
12631274
padded_A.view(np.uint32).ravel()[
12641275
np.add.outer(y_int * padded_A.shape[1], x_int)]
12651276
.view(np.uint8).reshape((height, width, 4)))
1266-
return im, l, b, IdentityTransform()
1277+
return im, l / magnification, b / magnification, IdentityTransform()
12671278

12681279
def _check_unsampled_image(self):
12691280
return False
486 Bytes
Loading
46 Bytes
Binary file not shown.
-45 Bytes
Loading

lib/matplotlib/tests/baseline_images/test_axes/imshow.svg

Lines changed: 17 additions & 17 deletions
Loading
-3 Bytes
Binary file not shown.
-56 Bytes
Loading

lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.svg

Lines changed: 19 additions & 19 deletions
Loading
-1.11 KB
Loading

0 commit comments

Comments
 (0)