From 2ace1d78c6f33a16d8aa7de2028307254d3a0001 Mon Sep 17 00:00:00 2001 From: "Albert Y. Shih" Date: Wed, 14 May 2025 13:34:10 -0400 Subject: [PATCH 1/2] Fix an off-by-half-pixel bug when resampling under a nonaffine transform --- lib/matplotlib/tests/test_image.py | 39 ++++++++++++++++++++++++++++-- src/_image_wrapper.cpp | 8 +++--- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index cededdb1b83c..c12a79bc2011 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -9,7 +9,7 @@ import urllib.request import numpy as np -from numpy.testing import assert_array_equal +from numpy.testing import assert_allclose, assert_array_equal from PIL import Image import matplotlib as mpl @@ -18,7 +18,7 @@ from matplotlib.image import (AxesImage, BboxImage, FigureImage, NonUniformImage, PcolorImage) from matplotlib.testing.decorators import check_figures_equal, image_comparison -from matplotlib.transforms import Bbox, Affine2D, TransformedBbox +from matplotlib.transforms import Bbox, Affine2D, Transform, TransformedBbox import matplotlib.ticker as mticker import pytest @@ -1641,6 +1641,41 @@ def test__resample_valid_output(): resample(np.zeros((9, 9)), out) +@pytest.mark.parametrize("data, interpolation, expected", + [(np.array([[0.1, 0.3, 0.2]]), mimage.NEAREST, + np.array([[0.1, 0.1, 0.1, 0.3, 0.3, 0.3, 0.3, 0.2, 0.2, 0.2]])), + (np.array([[0.1, 0.3, 0.2]]), mimage.BILINEAR, + np.array([[0.1, 0.1, 0.15078125, 0.21096191, 0.27033691, + 0.28476562, 0.2546875, 0.22460938, 0.20002441, 0.20002441]])), + ] +) +def test_resample_nonaffine(data, interpolation, expected): + # Test that equivalent affine and nonaffine transforms resample the same + + # Create a simple affine transform for scaling the input array + affine_transform = Affine2D().scale(sx=expected.shape[1] / data.shape[1], sy=1) + + affine_result = np.empty_like(expected) + mimage.resample(data, affine_result, affine_transform, + interpolation=interpolation) + assert_allclose(affine_result, expected) + + # Create a nonaffine version of the same transform + # by compositing with a nonaffine identity transform + class NonAffineIdentityTransform(Transform): + input_dims = 2 + output_dims = 2 + + def inverted(self): + return self + nonaffine_transform = NonAffineIdentityTransform() + affine_transform + + nonaffine_result = np.empty_like(expected) + mimage.resample(data, nonaffine_result, nonaffine_transform, + interpolation=interpolation) + assert_allclose(nonaffine_result, expected, atol=5e-3) + + def test_axesimage_get_shape(): # generate dummy image to test get_shape method ax = plt.gca() diff --git a/src/_image_wrapper.cpp b/src/_image_wrapper.cpp index 0f7b0da88de8..87d2b3b288ec 100644 --- a/src/_image_wrapper.cpp +++ b/src/_image_wrapper.cpp @@ -54,7 +54,7 @@ _get_transform_mesh(const py::object& transform, const py::ssize_t *dims) /* TODO: Could we get away with float, rather than double, arrays here? */ /* Given a non-affine transform object, create a mesh that maps - every pixel in the output image to the input image. This is used + every pixel center in the output image to the input image. This is used as a lookup table during the actual resampling. */ // If attribute doesn't exist, raises Python AttributeError @@ -66,8 +66,10 @@ _get_transform_mesh(const py::object& transform, const py::ssize_t *dims) for (auto y = 0; y < dims[0]; ++y) { for (auto x = 0; x < dims[1]; ++x) { - *p++ = (double)x; - *p++ = (double)y; + // The convention for the supplied transform is that pixel centers + // are at 0.5, 1.5, 2.5, etc. + *p++ = (double)x + 0.5; + *p++ = (double)y + 0.5; } } From 77bdca4c5148ac6ccf55acc6635ebf147ec28213 Mon Sep 17 00:00:00 2001 From: "Albert Y. Shih" Date: Wed, 14 May 2025 21:05:39 -0400 Subject: [PATCH 2/2] Updated affected baseline images --- .../test_image/log_scale_image.pdf | Bin 3780 -> 3776 bytes .../test_image/log_scale_image.png | Bin 7914 -> 7942 bytes .../test_image/log_scale_image.svg | 163 ++++++++++-------- 3 files changed, 87 insertions(+), 76 deletions(-) diff --git a/lib/matplotlib/tests/baseline_images/test_image/log_scale_image.pdf b/lib/matplotlib/tests/baseline_images/test_image/log_scale_image.pdf index b338fce6ee5ae825276f7705483805cb5bb26b31..c2641985025135cf6a050c8f49b3429c3939333b 100644 GIT binary patch delta 1737 zcmb_b{ZCU@7#3$KLaB!#8wJ{Xdjautjt|ojptSS|Q<%g}P}I(-N0?KG=qy$VQvZU2BJ8k3WgllPqWeV_Mv zp3|W|qyC+daf!O%$)iJY&mL$VKZs^_)DL;i22J4&2sh_HNtLo4%`8-*n4Vdt=Ub*XDhDE&uB)3)XiRuIpa3+PS2}xpe7@ zi_0`W;3bX~3p-a8bQaI=_~MhZYjV%7%{lWGl$~0ib*jv|Byf6AQv=i7pxL+0+8k)( zEAQ;ysP1|?K`3mVv!gFXCp7&rOHp)i26wb~LD0GB$2@<(SeSpWQk~-(ejfB&=H0GM z=AkF)eE*ZcF=IwDdkm<@gi5q_#)QfZ=zZ2+uC>=`?b84W2Sf)*0j8@loq}l`w5Yg> zAg*l!R}sSL6S>xfTKis>w@W$0?1n3@!qmKO1JW7L5NkheK*vmINNcYLWE7BiOcOC3 z2gL{|YN5zNQK}ZjdEevGWP-ZRp|8f}Er=S~)|G(l1!NeIb%0y~q!AECYtJ+wSJvQ9 z36`?m>GH5wt-{lkxt*!eHn&0;<;QW}XqdM@t~CUZc0ek?(b)Y?eXT=ZgKZ~Jo`f?N zkUM}}_s=pJ=7l790;-++Yp}_ji zicxUwr;|KW8Jk-sxI+c=Ry-veiApGrfeoqgt;>u^qW*g$lD9wKa&Bdp)@}u47a)fL z3BxoQ(+Esce3V%tIByh}7A$0p4a;vqVkkUmWIb+vvT9jn6OjZ8@iAFIoRZB-5&&6+sbuwE0Pz4)>NkHGrkQ|@ z$8@95`JxPpX_)3>IuVL~8(XG9QMxL^JPm@P6rBM@mCxKA*~O(M)NepX42a+Cm}i1( zkrd09G&Z0KS?F|?L-~()zN9_rHoc`G6^Vj@MaFD%4|>b=EtIodL6~WuZ?3Qmo6B;nI1NGJCvE7aSiy IJ3l-4KQE?xkpKVy delta 1717 zcmb_beM}Q)7^gB?D6||zq9$R#FfY}gU6=f5Z9g$)IVT&isxSVbsu*2-n{m0(CdGaPto;<&= z_lmYldmax;)nzDCrzet#=Ef%v<0l`+jg4x2qnhDS%^hFtZC_lUFaGK?u9g-2xc#y> z#pMdpFaN9_|Ja$}KJ?c(cs@>ww6Fxe)|91Wdw+pI%TJIv|iz17<-`Ut*TG;jOx*y8(u2`v# zU9R==w9rrM@iUHu6Nd=4(dCf4>7BTDx3}nM|9Sg?WSU!v7;>eL-?Tu!O&Z7eNLRg+I$M|8AOT_pB za>N$pw$!0~BRo!UeFWD-a0Mi{8{_YuYP-=&`Orp-#B#oBS zEsZ(BFuuCE^2@c|9fdki2+)VK(-`n*fj@(~e-5+_NP>~vMY804nA=i=ymsVuGVaUJ z776xY;Fz_UU(5&*jF3i&(()kc?BnFNDq;?IAjY4>_#%v#F~U;H76h8Ik+1{*qw?|Z zmk#xwvZHh}4Nse4nM9P}`d?_4U5m2oP`arLrRnrCR_jEQ^~`o3w1ok^WM?2dq>9-5 zC;BDpwbHMRFsBwx)}UesvP!nqp_wptXaRfDi8>?>(m*g<9S!_}lr51G!zfYD2%9m! zALA>Ce?3p3Ngdkqc6domK&V2qnHk z*`g^inX+ZQa43@!!x?!`bh3ABRpb)$H}=w zEg`rG0!Sf}+=~Z(Iu`gB0>6YzO^Gj4Vl*S{paafn^$K>iEaC#oM=^qoVGj3B+L77} zt4#2siFyy(0)Sr$JZeyqn3$=XN1^94{6Ean^Ta=22=u(voA35aUHpH0_cZ5`VN&6` y6sBFETj?4IYn82Z?Nm)gtss`p9l}Z|Up$sBTef_qt5TgCvLZvdc=4KoT;)F&V3u+K diff --git a/lib/matplotlib/tests/baseline_images/test_image/log_scale_image.png b/lib/matplotlib/tests/baseline_images/test_image/log_scale_image.png index 9d93c8fb00bfa8154ba15559c2ff83003f494efb..1df80c1b2045d77a8cba3bb4834c9c151aae7526 100644 GIT binary patch literal 7942 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 zd430$�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 - - + + + + + + 2025-05-14T18:02:41.587512 + image/svg+xml + + + Matplotlib v3.11.0.dev832+gc5ea66e278, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,100 +35,100 @@ L 518.4 388.8 L 518.4 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - + - + - + - + - + - + - + @@ -126,248 +137,248 @@ L 0 4 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - + - + - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -375,8 +386,8 @@ L -2 0 - - + +