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

Skip to content

Commit 3dea5c7

Browse files
committed
MNT: also transform vmin/vmax when rendering imshow
We use the Agg resampling routines to resample the user supplied data to the number of pixels we need in the final rendering. The Agg routines require that the input be in the range [0, 1] and it aggressively clips input and output to that range. Thus, we rescale the user data to [0, 1], pass it to the resampling routine and then rescale the result back to the original range. The resampled (shape wise) data is than passed to the user supplied norm to normalize the data to [0, 1] (again), and then onto the color map to get to RBGA. Due to float precision, the first re-scaling does not round-trip exactly in all casses. The error is extremely small (8-16 orders of magnitude smaller than the data) but for values that are exactly equal to the user supplied vmin or vmax this can be enough to push the data out of the "valid" gamut and be marked as "over" or "under". The colormaps default to using the top/bottom color for the over/under color so this is not visible, however if the user sets the over/under colors of the cmap this issue will be visible. closes #16910
1 parent c3bfeb9 commit 3dea5c7

File tree

2 files changed

+65
-5
lines changed

2 files changed

+65
-5
lines changed

lib/matplotlib/image.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -460,15 +460,36 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
460460
if newmax is not None or newmin is not None:
461461
np.clip(A_scaled, newmin, newmax, out=A_scaled)
462462

463+
# used to rescale the raw data to [offset, 1-offset]
464+
# so that the resampling code will run cleanly. Using
465+
# dyadic numbers here could reduce the error, but
466+
# would not full eliminate it and breaks a number of
467+
# tests (due to the slightly different error bouncing
468+
# some pixels across a boundary in the (very
469+
# quantized) color mapping step).
470+
offset = .1
471+
frac = .8
472+
# we need to run the vmin/vmax through the same rescaling
473+
# that we run the raw data through because there are small
474+
# errors in the round-trip due to float precision. If we
475+
# do not run the vmin/vmax through the same pipeline we can
476+
# have values close or equal to the boundaries end up on the
477+
# wrong side.
478+
vrange = np.array([self.norm.vmin, self.norm.vmax],
479+
dtype=scaled_dtype)
480+
463481
A_scaled -= a_min
482+
vrange -= a_min
464483
# a_min and a_max might be ndarray subclasses so use
465484
# item to avoid errors
466485
a_min = a_min.astype(scaled_dtype).item()
467486
a_max = a_max.astype(scaled_dtype).item()
468487

469488
if a_min != a_max:
470-
A_scaled /= ((a_max - a_min) / 0.8)
471-
A_scaled += 0.1
489+
A_scaled /= ((a_max - a_min) / frac)
490+
vrange /= ((a_max - a_min) / frac)
491+
A_scaled += offset
492+
vrange += offset
472493
# resample the input data to the correct resolution and shape
473494
A_resampled = _resample(self, A_scaled, out_shape, t)
474495
# done with A_scaled now, remove from namespace to be sure!
@@ -478,10 +499,13 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
478499
# below the original min/max will still be above /
479500
# below, but possibly clipped in the case of higher order
480501
# interpolation + drastically changing data.
481-
A_resampled -= 0.1
502+
A_resampled -= offset
503+
vrange -= offset
482504
if a_min != a_max:
483-
A_resampled *= ((a_max - a_min) / 0.8)
505+
A_resampled *= ((a_max - a_min) / frac)
506+
vrange *= ((a_max - a_min) / frac)
484507
A_resampled += a_min
508+
vrange += a_min
485509
# if using NoNorm, cast back to the original datatype
486510
if isinstance(self.norm, mcolors.NoNorm):
487511
A_resampled = A_resampled.astype(A.dtype)
@@ -508,7 +532,14 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
508532
out_alpha *= _resample(self, alpha, out_shape,
509533
t, resample=True)
510534
# mask and run through the norm
511-
output = self.norm(np.ma.masked_array(A_resampled, out_mask))
535+
resampled_masked = np.ma.masked_array(A_resampled, out_mask)
536+
# we have re-set the vmin/vmax to account for small errors
537+
# that may have moved input values in/out of range
538+
with cbook._setattr_cm(self.norm,
539+
vmin=vrange[0],
540+
vmax=vrange[1],
541+
):
542+
output = self.norm(resampled_masked)
512543
else:
513544
if A.shape[2] == 3:
514545
A = _rgb_to_rgba(A)

lib/matplotlib/tests/test_image.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,3 +1082,32 @@ def test_image_array_alpha(fig_test, fig_ref):
10821082
rgba = cmap(colors.Normalize()(zz))
10831083
rgba[..., -1] = alpha
10841084
ax.imshow(rgba, interpolation='nearest')
1085+
1086+
1087+
@pytest.mark.style('mpl20')
1088+
def test_exact_vmin():
1089+
cmap = copy(plt.cm.get_cmap("autumn_r"))
1090+
cmap.set_under(color="lightgrey")
1091+
1092+
# make the image exactly 190 pixels wide
1093+
fig = plt.figure(figsize=(1.9, 0.1), dpi=100)
1094+
ax = fig.add_axes([0, 0, 1, 1])
1095+
1096+
data = np.array(
1097+
[[-1, -1, -1, 0, 0, 0, 0, 43, 79, 95, 66, 1, -1, -1, -1, 0, 0, 0, 34]],
1098+
dtype=float,
1099+
)
1100+
1101+
im = ax.imshow(data, aspect="auto", cmap=cmap, vmin=0, vmax=100)
1102+
ax.axis("off")
1103+
fig.canvas.draw()
1104+
1105+
# get the RGBA slice from the image
1106+
from_image = im.make_image(fig.canvas.renderer)[0][0]
1107+
# expand the input to be 190 long and run through norm / cmap
1108+
direct_computation = (
1109+
im.cmap(im.norm((data * ([[1]] * 10)).T.ravel())) * 255
1110+
).astype(int)
1111+
1112+
# check than the RBGA values are the same
1113+
assert np.all(from_image == direct_computation)

0 commit comments

Comments
 (0)