Skip to content

Commit b2b37fb

Browse files
authored
Merge pull request #6581 from tacaswell/fix_over_under_images
FIX: colormapping of integer normed images
2 parents 07f10c4 + 0a940c4 commit b2b37fb

File tree

4 files changed

+94
-27
lines changed

4 files changed

+94
-27
lines changed

examples/pylab_examples/image_masked.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
The second subplot illustrates the use of BoundaryNorm to
55
get a filled contour effect.
66
"""
7-
7+
from copy import copy
88
from numpy import ma
99
import matplotlib.colors as colors
1010
import matplotlib.pyplot as plt
1111
import matplotlib.mlab as mlab
1212
import numpy as np
1313

14+
# compute some interesting data
1415
delta = 0.025
1516
x = y = np.arange(-3.0, 3.0, delta)
1617
X, Y = np.meshgrid(x, y)
@@ -19,7 +20,8 @@
1920
Z = 10*(Z2 - Z1) # difference of Gaussians
2021

2122
# Set up a colormap:
22-
palette = plt.cm.gray
23+
# use copy so that we do not mutate the global colormap instance
24+
palette = copy(plt.cm.gray)
2325
palette.set_over('r', 1.0)
2426
palette.set_under('g', 1.0)
2527
palette.set_bad('b', 1.0)
@@ -35,22 +37,25 @@
3537
# range to which the regular palette color scale is applied.
3638
# Anything above that range is colored based on palette.set_over, etc.
3739

38-
plt.subplot(1, 2, 1)
39-
im = plt.imshow(Zm, interpolation='bilinear',
40+
# set up the axes
41+
fig, (ax1, ax2) = plt.subplots(1, 2)
42+
43+
# plot using 'continuous' color map
44+
im = ax1.imshow(Zm, interpolation='bilinear',
4045
cmap=palette,
4146
norm=colors.Normalize(vmin=-1.0, vmax=1.0, clip=False),
4247
origin='lower', extent=[-3, 3, -3, 3])
43-
plt.title('Green=low, Red=high, Blue=bad')
44-
plt.colorbar(im, extend='both', orientation='horizontal', shrink=0.8)
48+
ax1.set_title('Green=low, Red=high, Blue=bad')
49+
fig.colorbar(im, extend='both', orientation='horizontal', shrink=0.8, ax=ax1)
4550

46-
plt.subplot(1, 2, 2)
47-
im = plt.imshow(Zm, interpolation='nearest',
51+
# plot using 'discrete' color map
52+
im = ax2.imshow(Zm, interpolation='nearest',
4853
cmap=palette,
4954
norm=colors.BoundaryNorm([-1, -0.5, -0.2, 0, 0.2, 0.5, 1],
5055
ncolors=256, clip=False),
5156
origin='lower', extent=[-3, 3, -3, 3])
52-
plt.title('With BoundaryNorm')
53-
plt.colorbar(im, extend='both', spacing='proportional',
54-
orientation='horizontal', shrink=0.8)
57+
ax2.set_title('With BoundaryNorm')
58+
fig.colorbar(im, extend='both', spacing='proportional',
59+
orientation='horizontal', shrink=0.8, ax=ax2)
5560

5661
plt.show()

lib/matplotlib/image.py

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -356,21 +356,42 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
356356
if not unsampled:
357357
created_rgba_mask = False
358358

359+
if A.ndim not in (2, 3):
360+
raise ValueError("Invalid dimensions, got %s" % (A.shape,))
361+
359362
if A.ndim == 2:
360363
A = self.norm(A)
361-
# If the image is greyscale, convert to RGBA with the
362-
# correct alpha channel for resizing
363-
rgba = np.empty((A.shape[0], A.shape[1], 4), dtype=A.dtype)
364-
rgba[..., 0:3] = np.expand_dims(A, 2)
365364
if A.dtype.kind == 'f':
366-
rgba[..., 3] = ~A.mask
365+
# If the image is greyscale, convert to RGBA and
366+
# use the extra channels for resizing the over,
367+
# under, and bad pixels. This is needed because
368+
# Agg's resampler is very aggressive about
369+
# clipping to [0, 1] and we use out-of-bounds
370+
# values to carry the over/under/bad information
371+
rgba = np.empty((A.shape[0], A.shape[1], 4), dtype=A.dtype)
372+
rgba[..., 0] = A # normalized data
373+
rgba[..., 1] = A < 0 # under data
374+
rgba[..., 2] = A > 1 # over data
375+
rgba[..., 3] = ~A.mask # bad data
376+
A = rgba
377+
output = np.zeros((out_height, out_width, 4),
378+
dtype=A.dtype)
379+
alpha = 1.0
380+
created_rgba_mask = True
367381
else:
368-
rgba[..., 3] = np.where(A.mask, 0, np.iinfo(A.dtype).max)
369-
A = rgba
370-
output = np.zeros((out_height, out_width, 4), dtype=A.dtype)
371-
alpha = 1.0
372-
created_rgba_mask = True
373-
elif A.ndim == 3:
382+
# colormap norms that output integers (ex NoNorm
383+
# and BoundaryNorm) to RGBA space before
384+
# interpolating. This is needed due to the
385+
# Agg resampler only working on floats in the
386+
# range [0, 1] and because interpolating indexes
387+
# into an arbitrary LUT may be problematic.
388+
#
389+
# This falls back to interpolating in RGBA space which
390+
# can produce it's own artifacts of colors not in the map
391+
# showing up in the final image.
392+
A = self.cmap(A, alpha=self.get_alpha(), bytes=True)
393+
394+
if not created_rgba_mask:
374395
# Always convert to RGBA, even if only RGB input
375396
if A.shape[2] == 3:
376397
A = _rgb_to_rgba(A)
@@ -382,8 +403,6 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
382403
alpha = self.get_alpha()
383404
if alpha is None:
384405
alpha = 1.0
385-
else:
386-
raise ValueError("Invalid dimensions, got %s" % (A.shape,))
387406

388407
_image.resample(
389408
A, output, t, _interpd_[self.get_interpolation()],
@@ -393,8 +412,13 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
393412
if created_rgba_mask:
394413
# Convert back to a masked greyscale array so
395414
# colormapping works correctly
415+
hid_output = output
396416
output = np.ma.masked_array(
397-
output[..., 0], output[..., 3] < 0.5)
417+
hid_output[..., 0], hid_output[..., 3] < 0.5)
418+
# relabel under data
419+
output[hid_output[..., 1] > .5] = -1
420+
# relabel over data
421+
output[hid_output[..., 2] > .5] = 2
398422

399423
output = self.to_rgba(output, bytes=True, norm=False)
400424

Loading

lib/matplotlib/tests/test_image.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@
2424
from numpy.testing import (
2525
assert_array_equal, assert_array_almost_equal, assert_allclose)
2626
from matplotlib.testing.noseclasses import KnownFailureTest
27-
28-
27+
from copy import copy
28+
from numpy import ma
29+
import matplotlib.colors as colors
30+
import matplotlib.pyplot as plt
31+
import matplotlib.mlab as mlab
32+
import numpy as np
2933

3034
import nose
3135

@@ -658,6 +662,40 @@ def test_image_preserve_size2():
658662
np.identity(n, bool)[::-1])
659663

660664

665+
@image_comparison(baseline_images=['mask_image_over_under'],
666+
remove_text=True, extensions=['png'])
667+
def test_mask_image_over_under():
668+
delta = 0.025
669+
x = y = np.arange(-3.0, 3.0, delta)
670+
X, Y = np.meshgrid(x, y)
671+
Z1 = mlab.bivariate_normal(X, Y, 1.0, 1.0, 0.0, 0.0)
672+
Z2 = mlab.bivariate_normal(X, Y, 1.5, 0.5, 1, 1)
673+
Z = 10*(Z2 - Z1) # difference of Gaussians
674+
675+
palette = copy(plt.cm.gray)
676+
palette.set_over('r', 1.0)
677+
palette.set_under('g', 1.0)
678+
palette.set_bad('b', 1.0)
679+
Zm = ma.masked_where(Z > 1.2, Z)
680+
fig, (ax1, ax2) = plt.subplots(1, 2)
681+
im = ax1.imshow(Zm, interpolation='bilinear',
682+
cmap=palette,
683+
norm=colors.Normalize(vmin=-1.0, vmax=1.0, clip=False),
684+
origin='lower', extent=[-3, 3, -3, 3])
685+
ax1.set_title('Green=low, Red=high, Blue=bad')
686+
fig.colorbar(im, extend='both', orientation='horizontal',
687+
ax=ax1, aspect=10)
688+
689+
im = ax2.imshow(Zm, interpolation='nearest',
690+
cmap=palette,
691+
norm=colors.BoundaryNorm([-1, -0.5, -0.2, 0, 0.2, 0.5, 1],
692+
ncolors=256, clip=False),
693+
origin='lower', extent=[-3, 3, -3, 3])
694+
ax2.set_title('With BoundaryNorm')
695+
fig.colorbar(im, extend='both', spacing='proportional',
696+
orientation='horizontal', ax=ax2, aspect=10)
697+
698+
661699
@image_comparison(baseline_images=['mask_image'],
662700
remove_text=True)
663701
def test_mask_image():

0 commit comments

Comments
 (0)