Skip to content

Commit f117602

Browse files
authored
Merge pull request matplotlib#31313 from ayshih/round_half_down
Fixed lingering bugs with image rendering related to exact half display pixels
2 parents 529da92 + 012e83f commit f117602

9 files changed

Lines changed: 102 additions & 19 deletions

File tree

lib/matplotlib/image.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,15 @@ def _resample(
209209

210210
# When an output pixel falls exactly on the edge between two input pixels, the Agg
211211
# 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.
212+
# left input pixel to be chosen instead, so we flip the input data and the supplied
213+
# transform. If origin != 'upper', the transform will already include a flip in the
214+
# vertical direction.
213215
if interpolation == 'nearest':
214-
transform += Affine2D().translate(-out.shape[1], -out.shape[0]).scale(-1, -1)
216+
transform = Affine2D().translate(-data.shape[1], 0).scale(-1, 1) + transform
217+
data = np.flip(data, axis=1)
218+
if image_obj.origin == 'upper':
219+
transform = Affine2D().translate(0, -data.shape[0]).scale(1, -1) + transform
220+
data = np.flip(data, axis=0)
215221

216222
_image.resample(data, out, transform,
217223
_interpd_[interpolation],
@@ -220,10 +226,6 @@ def _resample(
220226
image_obj.get_filternorm(),
221227
image_obj.get_filterrad())
222228

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-
227229
return out
228230

229231

@@ -408,7 +410,13 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
408410
magnified_extents = clipped_bbox.extents * magnification
409411
if ((not unsampled) and round_to_pixel_border):
410412
# Round to the nearest output pixel
411-
magnified_bbox = Bbox.from_extents((magnified_extents + 0.5).astype(int))
413+
# Add a tiny fudge amount to account for numerical precision loss
414+
# on the two sides away from the Agg anchor point (x0, y1)
415+
x0 = np.floor(magnified_extents[0] + 0.5) # round half up
416+
y0 = np.ceil(magnified_extents[1] - 0.5 - 1e-8) # round half down
417+
x1 = np.floor(magnified_extents[2] + 0.5 + 1e-8) # round half up
418+
y1 = np.ceil(magnified_extents[3] - 0.5) # round half down
419+
magnified_bbox = Bbox.from_extents([x0, y0, x1, y1])
412420
else:
413421
magnified_bbox = Bbox.from_extents(magnified_extents)
414422

-165 Bytes
Loading
-165 Bytes
Loading
50 KB
Loading
86 Bytes
Loading

lib/matplotlib/tests/test_image.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1677,6 +1677,8 @@ def test__resample_valid_output():
16771677
@pytest.mark.parametrize("data, interpolation, expected",
16781678
[(np.array([[0.1, 0.3, 0.2]]), mimage.NEAREST,
16791679
np.array([[0.1, 0.1, 0.1, 0.3, 0.3, 0.3, 0.3, 0.2, 0.2, 0.2]])),
1680+
(np.array([[0.1, 0.2, 0.3, 0.4, 0.5, 0.6]]), mimage.NEAREST,
1681+
np.array([[0.1, 0.2, 0.2, 0.3, 0.4, 0.4, 0.5, 0.6, 0.6]])),
16801682
(np.array([[0.1, 0.3, 0.2]]), mimage.BILINEAR,
16811683
np.array([[0.1, 0.1, 0.15, 0.21, 0.27, 0.285, 0.255, 0.225, 0.2, 0.2]])),
16821684
(np.array([[0.1, 0.9]]), mimage.BILINEAR,
@@ -1906,6 +1908,64 @@ def test_nn_pixel_alignment(nonaffine_identity):
19061908
axs[i, j].hlines(seps, -1, N, lw=0.5, color='red', ls='dashed')
19071909

19081910

1911+
@image_comparison(['alignment_half_display_pixels.png'], style='mpl20')
1912+
def test_alignment_half_display_pixels(nonaffine_identity):
1913+
# All values in this test are chosen carefully so that many display pixels are
1914+
# aligned with an edge or a corner of an input pixel
1915+
1916+
# Layout:
1917+
# Top row is origin='upper', bottom row is origin='lower'
1918+
# Column 1: affine transform, anchored at whole pixel
1919+
# Column 2: affine transform, anchored at half pixel
1920+
# Column 3: nonaffine transform, anchored at whole pixel
1921+
# Column 4: nonaffine transform, anchored at half pixel
1922+
# Column 5: affine transform, anchored at half pixel, interpolation='hanning'
1923+
1924+
# Each axes patch is magenta, so seeing a magenta line at an edge of the image
1925+
# means that the image is not filling the axes
1926+
1927+
fig = plt.figure(figsize=(5, 2), dpi=100)
1928+
fig.set_facecolor('g')
1929+
1930+
corner_x = [0.01, 0.199, 0.41, 0.599, 0.809]
1931+
corner_y = [0.05, 0.53]
1932+
1933+
axs = []
1934+
for cy in corner_y:
1935+
for ix, cx in enumerate(corner_x):
1936+
my = cy + 0.0125 if ix in [1, 3, 4] else cy
1937+
axs.append(fig.add_axes([cx, my, 0.17, 0.425], xticks=[], yticks=[]))
1938+
1939+
# Verify that each axes has been created with the correct width/height and that all
1940+
# four corners are on whole pixels (columns 1 and 3) or half pixels (columns 2, 4,
1941+
# and 5)
1942+
for i, ax in enumerate(axs):
1943+
extents = ax.get_window_extent().extents
1944+
assert_allclose(extents[2:4] - extents[0:2], 85, rtol=0, atol=1e-13)
1945+
assert_allclose(extents % 1, 0.5 if i % 5 in [1, 3, 4] else 0,
1946+
rtol=0, atol=1e-13)
1947+
1948+
N = 10
1949+
1950+
data = np.arange(N**2).reshape((N, N)) % 9
1951+
seps = np.arange(-0.5, N)
1952+
1953+
for i, ax in enumerate(axs):
1954+
ax.set_facecolor('m')
1955+
1956+
transform = nonaffine_identity + ax.transData if i % 4 >= 2 else ax.transData
1957+
ax.imshow(data, cmap='Blues',
1958+
interpolation='hanning' if i % 5 == 4 else 'nearest',
1959+
origin='upper' if i >= 5 else 'lower',
1960+
transform=transform)
1961+
1962+
ax.vlines(seps, -0.5, N - 0.5, lw=0.5, color='red', ls=(0, (2, 4)))
1963+
ax.hlines(seps, -0.5, N - 0.5, lw=0.5, color='red', ls=(0, (2, 4)))
1964+
1965+
for spine in ax.spines:
1966+
ax.spines[spine].set_linestyle((0, (5, 10)))
1967+
1968+
19091969
@image_comparison(['image_bounds_handling.png'], tol=0.006)
19101970
def test_image_bounds_handling(nonaffine_identity):
19111971
# TODO: The second and third panels in the bottom row show that the handling of
17 Bytes
Loading

src/_image_resample.h

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -589,8 +589,9 @@ class lookup_distortion
589589
if (dx >= 0 && dx < m_out_width &&
590590
dy >= 0 && dy < m_out_height) {
591591
const double *coord = m_mesh + (int(dy) * m_out_width + int(dx)) * 2;
592-
*x = int(coord[0] * agg::image_subpixel_scale + offset);
593-
*y = int(coord[1] * agg::image_subpixel_scale + offset);
592+
// Add a tiny fudge amount to account for numerical precision loss
593+
*x = int(coord[0] * agg::image_subpixel_scale + offset + 1e-8);
594+
*y = int(coord[1] * agg::image_subpixel_scale + offset + 1e-8);
594595
}
595596
}
596597
}
@@ -780,10 +781,15 @@ void resample(
780781
params.affine.transform(&right, &top);
781782
if (left > right) { std::swap(left, right); }
782783
if (bottom > top) { std::swap(top, bottom); }
783-
if (round(left) < left) { left = round(left); }
784-
if (round(right) > right) { right = round(right); }
785-
if (round(bottom) < bottom) { bottom = round(bottom); }
786-
if (round(top) > top) { top = round(top); }
784+
// Add a tiny fudge amount to account for numerical precision loss
785+
int rleft = agg::iround(left - 1e-8);
786+
int rright = agg::iround(right + 1e-8);
787+
int rbottom = agg::iround(bottom - 1e-8);
788+
int rtop = agg::iround(top + 1e-8);
789+
if (rleft < left) { left = rleft; }
790+
if (rright > right) { right = rright; }
791+
if (rbottom < bottom) { bottom = rbottom; }
792+
if (rtop > top) { top = rtop; }
787793
path.move_to(left, bottom);
788794
path.line_to(right, bottom);
789795
path.line_to(right, top);

src/agg_workaround.h

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ class accurate_interpolator_affine_nn
8080
void begin(double x, double y, unsigned len)
8181
{
8282
m_len = len - 1;
83+
m_cur = 0;
8384

8485
m_stx1 = x;
8586
m_sty1 = y;
@@ -98,6 +99,7 @@ class accurate_interpolator_affine_nn
9899
void resynchronize(double xe, double ye, unsigned len)
99100
{
100101
m_len = len - 1;
102+
m_cur = 0;
101103

102104
m_trans->transform(&xe, &ye);
103105
m_stx2 = xe * subpixel_scale;
@@ -107,23 +109,30 @@ class accurate_interpolator_affine_nn
107109
//----------------------------------------------------------------
108110
void operator++()
109111
{
110-
m_stx1 += (m_stx2 - m_stx1) / m_len;
111-
m_sty1 += (m_sty2 - m_sty1) / m_len;
112-
m_len--;
112+
m_cur++;
113113
}
114114

115115
//----------------------------------------------------------------
116116
void coordinates(int* x, int* y) const
117117
{
118118
// Truncate instead of round because this interpolator needs to
119119
// match the definitions for nearest-neighbor interpolation
120-
*x = int(m_stx1);
121-
*y = int(m_sty1);
120+
if (m_cur == m_len)
121+
{
122+
*x = int(m_stx2);
123+
*y = int(m_sty2);
124+
}
125+
else
126+
{
127+
// Add a tiny fudge amount to account for numerical precision loss
128+
*x = int(m_stx1 + (m_stx2 - m_stx1) * m_cur / m_len + 1e-8);
129+
*y = int(m_sty1 + (m_sty2 - m_sty1) * m_cur / m_len + 1e-8);
130+
}
122131
}
123132

124133
private:
125134
trans_type* m_trans;
126-
unsigned m_len;
135+
unsigned m_len, m_cur;
127136
double m_stx1, m_sty1, m_stx2, m_sty2;
128137
};
129138
#endif

0 commit comments

Comments
 (0)