You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Axes.imshow draws invalid color at value is 0 when max of 'X' not equal to vmax #16910
I think the report is very consistent. But we can make it more clear: Compare how the same data is plotted via imshow and pcolormesh. Of course we would expect both images to be equal.
My knee-jerk reaction is that this a numerical stability issue.
Possibly related to #10072 / #10133 . Chasing through the linked bug reports and PRs is a good place to start. b503d6a (2.1.x branch) or b511bb2 (on the master branch) are likely the offending commits.
# done with A_scaled now, remove from namespace to be sure!
delA_scaled
# un-scale the resampled data to approximately the
# original range things that interpolated to above /
# below the original min/max will still be above /
# below, but possibly clipped in the case of higher order
# interpolation + drastically changing data.
A_resampled-=0.1
ifa_min!=a_max:
A_resampled*= ((a_max-a_min) /0.8)
A_resampled+=a_min
The issue that code is trying to solve is clipping at the edges of the interpolation range so we take the input data, scale it to [.1, .9] , run it through the re-sampling routine, re sample (up or down) the data to match the screen resolution, then shift it back to the input range, then run it through the normalization (linear, log, etc), then color map it from [0, 1] -> RGBA, then draw that to the screen.
Pulling out the scale down / scale up round tripping
importnumpyasnpimportmatplotlib.pyplotaspltdeftest_broken(top, scaled_dtype=np.float32):
A=np.array([-1, 0, top], dtype=float)
a_min=A.min()
a_max=A.max()
A_scaled=np.array(A, dtype=scaled_dtype)
A_scaled-=a_minA_scaled/= (a_max-a_min) /0.8A_scaled+=0.1# un-scale the resampled data to approximately the# original range things that interpolated to above /# below the original min/max will still be above /# below, but possibly clipped in the case of higher order# interpolation + drastically changing data.A_scaled=np.array(A_scaled)
A_scaled-=0.1A_scaled*= (a_max-a_min) /0.8A_scaled+=a_minreturn (A_scaled-A)[1]
fig, (ax1, ax2) =plt.subplots(2, constrained_layout=True)
forax, sdtinzip((ax1, ax2), (np.float32, np.float64)):
data= [test_broken(n, scaled_dtype=sdt) forninrange(250)]
ax.plot(data, label=str(sdt))
ax.set_title(f'round-trip error of 0 with {sdt!r}')
ax.set_ylabel('error')
ax.set_xlabel('"0" value')
plt.show()
we see that we systematically miss the round trip of the 0 in the array (both above and below) based on what the maximum limits are (it would be interesting to extend this to do the matrix of varying the low value as well).
This looks like it is fundamental issue with float precision so tweaking the number on the way we do the scaling (the .1 and .8 constants could be tweaked) will avoid the problem in all cases. I am not enough of an expert on numerical computation to know if there are things we could be doing in this code to reduce the error.
We can not do the re-scaling for interpolation clipped to the vmin/vmax range because that causes very out-of-range points to contribute less (the vmid logic in the code is a compromise to handle very small vmin/vmax on a very large background).
I am going to move this to 3.4 as I suspect the real solution here is to (as @anntzer has been advocating for a while) is replace the Agg based interpolation with something that does not require us to pre-scale to [0, 1].
The very blunt work-around, if you know your data is going to be integers is to set the vmin/vmax to be a float just below and just above the min / max values.
I don't know if that helps? I guess that helps for the exact x=vmin case, but not sure if it helps for x = vmin + eps case, which I think could theoretically end up on the wrong side of vmin?
I hope that if a < b is true is the input data, then the worst case scenario after the transform is a <= b (and a > b should be impossible). Hence if x = vmin + eps it should still stay on "the right side" of the mapping?
Actually, I'm vaguely confused now. Why do we have to rescale to 0.1 0.9 rather than e.g. 0/1? We are separately resampling the mask anyways, so we should be able to use that info anyways to know what to mask out at the end, no? Also, what's the way resampling between an non-finite and a finite value is handled?
Lets just say vmin=-1 and vmax=1. If we renormalize from 0-1, then a value at -1.1 gets set to 0 and we lose the idea that it is "under" -1. But we want to keep track of the fact that its under, so hence the kludge.
But what where we doing with nans? They were mapped to something, no? And then masked out later, because we also resample the nan mask. So why can't we have an under/over mask as well?
Experimentally, running the same rescaling logic on vmin/vmax and the temporarily adjusting the vmin/vmax on the norm does indeed fix the OP.
We did have an over/under mask at one point, but we had to back out of that because when you use the non-trivial interpolations you may get values that are with in the allowed gamut but include values from the original data with is out of gamut. At one point we did mask any pixel which was interpolated from an out-of-gamut input as out of range and it very quickly got bug reports so we have to do the over/under classification after we do the resampling. We do poison invalid (nan or inf) points.
importnumpyasnpimportmatplotlib.pyplotaspltexp=5offset=1/(2**exp)
frac=1-2*offsetdeftest_broken(top, frac, offset, scaled_dtype=np.float32):
A=np.array([-1, 0, top], dtype=float)
a_min=A.min()
a_max=A.max()
A_scaled=np.array(A, dtype=scaled_dtype)
A_scaled-=a_minA_scaled/= (a_max-a_min) /fracA_scaled+=offset# un-scale the resampled data to approximately the# original range things that interpolated to above /# below the original min/max will still be above /# below, but possibly clipped in the case of higher order# interpolation + drastically changing data.A_scaled=np.array(A_scaled)
A_scaled-=offsetA_scaled*= (a_max-a_min) /fracA_scaled+=a_minreturn (A_scaled-A)[1]
fig, (ax1, ax2) =plt.subplots(2, constrained_layout=True)
forax, sdtinzip((ax1, ax2), (np.float32, np.float64)):
forexpin [2, 3, 4, 5]:
offset=1/(2**exp)
frac=1-2*offsetdata= [test_broken(n, frac, offset, scaled_dtype=sdt) forninrange(1000)]
ax.plot(data, label=f'{offset}')
ax.set_title(f'round-trip error of 0 with {sdt!r}')
ax.set_ylabel('error')
ax.set_xlabel('top value')
ax.legend(ncol=2)
plt.show()
is the corrected code for the last set of images I posted.
Activity
anntzer commentedon Mar 26, 2020
I assumed you meant min value not equal to vmin.
Here you explicitly requested that values below vmin are in lightgrey (`cmap.set_under('lightgrey')), so I think this is behaving as expected?
ImportanceOfBeingErnest commentedon Mar 26, 2020
I think the report is very consistent. But we can make it more clear: Compare how the same data is plotted via
imshowandpcolormesh. Of course we would expect both images to be equal.ImportanceOfBeingErnest commentedon Mar 26, 2020
This works with matplotlib 2.0.2, but fails with matplotlib 2.1.2 or higher. (Unfortunately, I'm unable to bisect)
Will mark as release critical for 3.3, though one might even consider this a bad enough bug to fix earlier.
tacaswell commentedon Mar 26, 2020
My knee-jerk reaction is that this a numerical stability issue.
Possibly related to #10072 / #10133 . Chasing through the linked bug reports and PRs is a good place to start. b503d6a (2.1.x branch) or b511bb2 (on the master branch) are likely the offending commits.
tacaswell commentedon Mar 26, 2020
That this now "works" for pcolormesh and not imshow suggests that the other bug reported in #10072 is still present in pcolormesh.
QuLogic commentedon Apr 29, 2020
Bisect says 12c27f3, i.e., #8966.
tacaswell commentedon Jun 12, 2020
I think the problem is
matplotlib/lib/matplotlib/image.py
Lines 469 to 484 in 15c0d7c
The issue that code is trying to solve is clipping at the edges of the interpolation range so we take the input data, scale it to [.1, .9] , run it through the re-sampling routine, re sample (up or down) the data to match the screen resolution, then shift it back to the input range, then run it through the normalization (linear, log, etc), then color map it from [0, 1] -> RGBA, then draw that to the screen.
Pulling out the scale down / scale up round tripping
we see that we systematically miss the round trip of the 0 in the array (both above and below) based on what the maximum limits are (it would be interesting to extend this to do the matrix of varying the low value as well).
This looks like it is fundamental issue with float precision so tweaking the number on the way we do the scaling (the .1 and .8 constants could be tweaked) will avoid the problem in all cases. I am not enough of an expert on numerical computation to know if there are things we could be doing in this code to reduce the error.
We can not do the re-scaling for interpolation clipped to the vmin/vmax range because that causes very out-of-range points to contribute less (the
vmidlogic in the code is a compromise to handle very small vmin/vmax on a very large background).I am going to move this to 3.4 as I suspect the real solution here is to (as @anntzer has been advocating for a while) is replace the Agg based interpolation with something that does not require us to pre-scale to [0, 1].
The very blunt work-around, if you know your data is going to be integers is to set the vmin/vmax to be a float just below and just above the min / max values.
12 remaining items
tacaswell commentedon Jun 15, 2020
🤦 Sorry, lost track of what I was doing, your right, it does not eliminate the problem.
jklymak commentedon Jun 15, 2020
So maybe something like this, with a smaller buffer, and then in
colors.pyaround line 571:though there is a funky statement above on line 561 that also needs to be changed.
EDIT: where I'm not sure 1e-16 is the right number at all, and I guess from the above its not for 32-bit images.
tacaswell commentedon Jun 15, 2020
Maybe the right thing to do is to run vmin and vmax through the same pipeline?
jklymak commentedon Jun 15, 2020
I don't know if that helps? I guess that helps for the exact x=vmin case, but not sure if it helps for x = vmin + eps case, which I think could theoretically end up on the wrong side of vmin?
tacaswell commentedon Jun 15, 2020
I hope that if
a < bis true is the input data, then the worst case scenario after the transform isa <= b(anda > bshould be impossible). Hence ifx = vmin + epsit should still stay on "the right side" of the mapping?jklymak commentedon Jun 15, 2020
Good question, but afraid I don't know the answer!
anntzer commentedon Jun 15, 2020
Actually, I'm vaguely confused now. Why do we have to rescale to 0.1 0.9 rather than e.g. 0/1? We are separately resampling the mask anyways, so we should be able to use that info anyways to know what to mask out at the end, no? Also, what's the way resampling between an non-finite and a finite value is handled?
jklymak commentedon Jun 15, 2020
Lets just say vmin=-1 and vmax=1. If we renormalize from 0-1, then a value at -1.1 gets set to 0 and we lose the idea that it is "under" -1. But we want to keep track of the fact that its under, so hence the kludge.
MNT: also transform vmin/vmax when rendering imshow
anntzer commentedon Jun 15, 2020
But what where we doing with nans? They were mapped to something, no? And then masked out later, because we also resample the nan mask. So why can't we have an under/over mask as well?
tacaswell commentedon Jun 15, 2020
Experimentally, running the same rescaling logic on vmin/vmax and the temporarily adjusting the vmin/vmax on the norm does indeed fix the OP.
We did have an over/under mask at one point, but we had to back out of that because when you use the non-trivial interpolations you may get values that are with in the allowed gamut but include values from the original data with is out of gamut. At one point we did mask any pixel which was interpolated from an out-of-gamut input as out of range and it very quickly got bug reports so we have to do the over/under classification after we do the resampling. We do poison invalid (nan or inf) points.
tacaswell commentedon Oct 7, 2020
For completeness
is the corrected code for the last set of images I posted.