8000 cleanup of _ImageBase._make_image() · matplotlib/matplotlib@c4925ec · GitHub
[go: up one dir, main page]

Skip to content

Commit c4925ec

Browse files
committed
cleanup of _ImageBase._make_image()
1 parent 107513a commit c4925ec

File tree

4 files changed

+177
-119
lines changed

4 files changed

+177
-119
lines changed

lib/matplotlib/backends/qt_editor/figureoptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def prepare_data(d, init):
155155
cmaps = [(cmap, name) for name, cmap in sorted(cm._bivar_colormaps.items())]
156156
cvals = cm._bivar_colormaps.values()
157157
else: # isinstance(mappable.get_cmap(), mcolors.MultivarColormap):
158-
cmaps = [(cmap, name) for name, cmap
158+
cmaps = [(cmap, name) for name, cmap
159159
in sorted(cm._multivar_colormaps.items())]
160160
cvals = cm._multivar_colormaps.values()
161161
if cmap not in cvals:

lib/matplotlib/cm.py

Lines changed: 85 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -430,42 +430,8 @@ def to_rgba(self, x, alpha=None, bytes=False, norm=True):
430430
# First check for special case, image input:
431431
try:
432432
if self.n_variates == 1 and x.ndim == 3:
433-
if x.shape[2] == 3:
434-
if alpha is None:
435-
alpha = 1
436-
if x.dtype == np.uint8:
437-
alpha = np.uint8(alpha * 255)
438-
m, n = x.shape[:2]
439-
xx = np.empty(shape=(m, n, 4), dtype=x.dtype)
440-
xx[:, :, :3] = x
441-
xx[:, :, 3] = alpha
442-
elif x.shape[2] == 4:
443-
xx = x
444-
else:
445-
raise ValueError("Third dimension must be 3 or 4")
446-
if xx.dtype.kind == 'f':
447-
# If any of R, G, B, or A is nan, set to 0
448-
if np.any(nans := np.isnan(x)):
449-
if x.shape[2] == 4:
450-
xx = xx.copy()
451-
xx[np.any(nans, axis=2), :] = 0
452-
453-
if norm and (xx.max() > 1 or xx.min() < 0):
454-
raise ValueError("Floating point image RGB values "
455-
"must be in the 0..1 range.")
456-
if bytes:
457-
xx = (xx * 255).astype(np.uint8)
458-
elif xx.dtype == np.uint8:
459-
if not bytes:
460-
xx = xx.astype(np.float32) / 255
461-
else:
462-
raise ValueError("Image RGB array must be uint8 or "
463-
"floating point; found %s" % xx.dtype)
464-
# Account for any masked entries in the original array
465-
# If any of R, G, B, or A are masked for an entry, we set alpha to 0
466-
if np.ma.is_masked(x):
467-
xx[np.any(np.ma.getmaskarray(x), axis=2), 3] = 0
468-
return xx
433+
# looks like imega data, try to process it without cmap
434+
return self._pass_image_data(x, alpha, bytes, norm)
469435
except AttributeError:
470436
# e.g., x is not an ndarray; so try mapping it
471437
pass
@@ -474,16 +440,93 @@ def to_rgba(self, x, alpha=None, bytes=False, norm=True):
474440
if self.cmap.n_variates == 1:
475441
x = ma.asarray(x)
476442
if norm:
477-
x = self._norm[0](x)
443+
x = self.normalize(x)
478444
rgba = self.cmap(x, alpha=alpha, bytes=bytes)
479445
else:
480-
x = _ensure_multivariate_data(self.cmap.n_variates, x)
481-
x = _iterable_variates_in_data(x)
482446
if norm:
483-
x = [norm(xx) for norm, xx in zip(self._norm, x)]
447+
x = self.normalize(x)
484448
rgba = self.cmap(x, alpha=alpha, bytes=bytes)
485449
return rgba
486450

451+
@staticmethod
452+
def _pass_image_data(x, alpha=None, bytes=False, norm=True):
453+
"""
454+
Helper function to pass ndarray of shape (...,3) or (..., 4)
455+
through `to_rgba()`, see `to_rgba()` for docstring.
456+
"""
457+
if x.shape[2] == 3:
458+
if alpha is None:
459+
alpha = 1
460+
if x.dtype == np.uint8:
461+
alpha = np.uint8(alpha * 255)
462+
m, n = x.shape[:2]
463+
xx = np.empty(shape=(m, n, 4), dtype=x.dtype)
464+
xx[:, :, :3] = x
465+
xx[:, :, 3] = alpha
466+
elif x.shape[2] == 4:
467+
xx = x
468+
else:
469+
raise ValueError("Third dimension must be 3 or 4")
470+
if xx.dtype.kind == 'f':
471+
# If any of R, G, B, or A is nan, set to 0
472+
if np.any(nans := np.isnan(x)):
473+
if x.shape[2] == 4:
474+
xx = xx.copy()
475+
xx[np.any(nans, axis=2), :] = 0
476+
477+
if norm and (xx.max() > 1 or xx.min() < 0):
478+
raise ValueError("Floating point image RGB values "
479+
"must be in the 0..1 range.")
480+
if bytes:
481+
xx = (xx * 255).astype(np.uint8)
482+
elif xx.dtype == np.uint8:
483+
if not bytes:
484+
xx = xx.astype(np.float32) / 255
485+
else:
486+
raise ValueError("Image RGB array must be uint8 or "
487+
"floating point; found %s" % xx.dtype)
488+
# Account for any masked entries in the original array
489+
# If any of R, G, B, or A are masked for an entry, we set alpha to 0
490+
if np.ma.is_masked(x):
491+
xx[np.any(np.ma.getmaskarray(x), axis=2), 3] = 0
492+
return xx
493+
494+
def normalize(self, x):
495+
"""
496+
Normalize the data in x.
497+
498+
Parameters
499+
----------
500+
x : np.array or sequence of arrays. Must be compatible with the number
501+
of variates (`NormAndColor.n_variates`).
502+
503+
- If there is a single norm, x may be of any shape.
504+
505+
- If there are two norms x may be a sequce of length 2, an array with
506+
complex numbers, or an array with a dtype containing two fields
507+
508+
- If there more than two norms, x may be a sequce of length n, or an array
509+
with a dtype containing n fields.
510+
511+
Returns
512+
-------
513+
np.array, or if more than one variate, a list of np.arrays.
514+
515+
"""
516+
if self.n_variates == 1:
517+
return self._norm[0](x)
518+
elif hasattr(x, 'dtype') and len(x.dtype.descr) > 1:
519+
x = _iterable_variates_in_data(x)
520+
elif np.iscomplexobj(x):
521+
# NOTE: when data is passed to plotting methods, i.e.
522+
# imshow(data), and the data is complex, it is converted
523+
# to a dtype with two fields.
524+
# Therefore, complex data should only arrive here if
525+
# the user invokes VectorMappable.to_rgba(data) or
526+
# NormAndColor.to_rgba(data) etc. with complex data directly.
527+
x = [x.real, x.imag]
528+
return [norm(xx) for norm, xx in zip(self._norm, x)]
529+
487530
def autoscale(self, A):
488531
"""
489532
Autoscale the scalar limits on the norm instance using the
@@ -505,11 +548,8 @@ def autoscale_None(self, A):
505548
raise TypeError('You must first set_array for mappable')
506549
# If the norm's limits are updated self.changed() will be called
507550
# through the callbacks attached to the norm
508-
if self.cmap.n_variates == 1:
509-
self._norm[0].autoscale_None(A)
510-
else:
511-
for n, a in zip(self._norm, _iterable_variates_in_data(A)):
512-
n.autoscale_None(a)
551+
for n, a in zip(self._norm, _iterable_variates_in_data(A)):
552+
n.autoscale_None(a)
513553

514554
def _set_cmap(self, cmap):
515555
"""

lib/matplotlib/colors.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,7 @@ def _get_rgba_and_mask(self, X, alpha=None, bytes=False):
765765
if not self._isinit:
766766
self._init()
767767

768+
mask_bad = _get_mask([X])
768769
xa = np.array(X, copy=True)
769770
if not xa.dtype.isnative:
770771
# Native byteorder is faster.
@@ -778,7 +779,6 @@ def _get_rgba_and_mask(self, X, alpha=None, bytes=False):
778779
mask_under = xa < 0
779780
mask_over = xa >= self.N
780781
# If input was masked, get the bad mask from it; else mask out nans.
781-
mask_bad = X.mask if np.ma.is_masked(X) else np.isnan(xa)
782782
with np.errstate(invalid="ignore"):
783783
# We need this cast for unsigned ints as well as floats
784784
xa = xa.astype(int)
@@ -1333,6 +1333,9 @@ def __call__(self, X, alpha=None, bytes=False, clip=True):
13331333
rgba, mask_bad = self[0]._get_rgba_and_mask(X[0], bytes=False)
13341334
rgba = np.asarray(rgba)
13351335
for c, xx in zip(self[1:], X[1:]):
1336+
# because the components already calculate the mask
1337+
# we do not call `_get_mask(X)` here, but instead combine the
1338+
# existing masks.
13361339
sub_rgba, sub_mask_bad = c._get_rgba_and_mask(xx, bytes=False)
13371340
sub_rgba = np.asarray(sub_rgba)
13381341
rgba[..., :3] += sub_rgba[..., :3] # add colors
@@ -1623,9 +1626,7 @@ def __call__(self, X, alpha=None, bytes=False):
16231626
mask_outside = (X0 < 0) | (X1 < 0) \
16241627
| (X0 >= self.N) | (X1 >= self.M)
16251628
# If input was masked, get the bad mask from it; else mask out nans.
1626-
mask_bad_0 = X0.mask if np.ma.is_masked(X0) else np.isnan(X0)
1627-
mask_bad_1 = X1.mask if np.ma.is_masked(X1) else np.isnan(X1)
1628-
mask_bad = mask_bad_0 | mask_bad_1
1629+
mask_bad = _get_mask([X0, X1])
16291630

16301631
with np.errstate(invalid="ignore"):
16311632
# We need this cast for unsigned ints as well as floats
@@ -2725,6 +2726,28 @@ def autoscale_None(self, A):
27252726
return Norm
27262727

27272728

2729+
def _get_mask(X):
2730+
"""
2731+
Helper function to get the mask. Marked values and np.nan are
2732+
true in the output
2733+
2734+
Parameters
2735+
----------
2736+
X : iterable of np.arrays
2737+
each array must be numpy array or masked array of shape
2738+
(n, m) or (n)
2739+
2740+
Returns
2741+
-------
2742+
mask, bool or boolean np.array of shape (n,m) or (n)
2743+
"""
2744+
mask_bad = X[0].mask if np.ma.is_masked(X[0]) else np.isnan(X[0])
2745+
if len(X) > 1:
2746+
for xx in X[1:]:
2747+
mask_bad |= xx.mask if np.ma.is_masked(xx) else np.isnan(xx)
2748+
return mask_bad
2749+
2750+
27282751
def _create_empty_object_of_class(cls):
27292752
return cls.__new__(cls)
27302753

lib/matplotlib/image.py

Lines changed: 64 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -487,86 +487,81 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
487487
interpolation_stage = 'rgba'
488488
else:
489489
interpolation_stage = 'data'
490-
491-
if A.ndim == 2 and interpolation_stage == 'data':
492-
# if we are a 2D array, then we are running through the
493-
# norm + colormap transformation. However, in general the
494-
# input data is not going to match the size on the screen so we
495-
# have to resample to the correct number of pixels
496-
497-
# First compute out_mask (what screen pixels include "bad" data
498-
# pixels) and out_alpha (to what extent screen pixels are
499-
# covered by data pixels: 0 outside the data extent, 1 inside
500-
# (even for bad data), and intermediate values at the edges).
501-
if self.cmap.n_variates == 1:
502-
mask = (np.where(A.mask, np.float32(np.nan), np.float32(1))
503-
if A.mask.shape == A.shape # nontrivial mask
504-
else np.ones_like(A, np.float32))
505-
else:
506-
_maskNd = [(np.where(a.mask, np.float32(np.nan), np.float32(1))
507-
if A.mask.shape == a.shape # nontrivial mask
508-
else np.ones_like(a, np.float32))
509-
for a in cm._iterable_variates_in_data(A)]
510-
mask = np.prod(_maskNd, axis=0)
511-
512-
# we always have to interpolate the mask to account for
513-
# non-affine transformations
514-
out_alpha = _resample(self, mask, out_shape, t, resample=True)
515-
del mask # Make sure we don't use mask anymore!
516-
out_mask = np.isnan(out_alpha)
517-
out_alpha[out_mask] = 1
518-
# Apply the pixel-by-pixel alpha values if present
519-
alpha = self.get_alpha()
520-
if alpha is not None and np.ndim(alpha) > 0:
521-
out_alpha *= _resample(self, alpha, out_shape, t, resample=True)
522-
# Resample data
523-
if self.cmap.n_variates == 1:
524-
output = self._resample_and_norm(A, self.norm, out_shape,
525-
out_mask, t)
526-
else:
527-
output = [self._resample_and_norm(a, self.norm[i],
528-
out_shape, out_mask, t)
529-
for i, a in enumerate(cm._iterable_variates_in_data(A))]
530-
else:
531-
if A.ndim == 2: # _interpolation_stage == 'rgba'
490+
scalar_alpha = self._get_scalar_alpha()
491+
if A.ndim == 2:
492+
if interpolation_stage == 'rgba':
493+
# run norm -> colormap transformation and then rescale
494+
self.nac.autoscale_None(A)
495+
unscaled_rgba = self.nac.to_rgba(A)
496+
output = _resample( # resample rgba channels
497+
self, unscaled_rgba, out_shape, t, alpha=scalar_alpha)
498+
# transforming to bytes *after* resampling gives improved results
499+
if output.dtype.kind == 'f':
500+
output = (output * 255).astype(np.uint8)
501+
else: # if _interpolation_stage != 'rgba':
502+
# In general the input data is not going to match the size on the
503+
# screen so we have to resample to the correct number of pixels
504+
505+
# First compute out_mask (what screen pixels include "bad" data
506+
# pixels) and out_alpha (to what extent screen pixels are
507+
# covered by data pixels: 0 outside the data extent, 1 inside
508+
# (even for bad data), and intermediate values at the edges).
509+
mask = mcolors._get_mask(cm._iterable_variates_in_data(A))
510+
if mask.shape == A.shape:
511+
# we always have to interpolate the mask to account for
512+
# non-affine transformations
513+
# To get all pixels where partially covered by the mask
514+
# we run _resample with an array with np.nan
515+
nan_mask = np.where(mask, np.float32(np.nan), np.float32(1))
516+
out_alpha = _resample(self, nan_mask, out_shape, t,
517+
resample=True)
518+
else:
519+
out_alpha = np.ones(out_shape, np.float32)
520+
del mask, nan_mask # Make sure we don't use mask anymore!
521+
out_mask = np.isnan(out_alpha)
522+
out_alpha[out_mask] = 1
523+
# Apply the pixel-by-pixel alpha values if present
524+
alpha = self.get_alpha()
525+
if alpha is not None and np.ndim(alpha) > 0:
526+
out_alpha *= _resample(self, alpha, out_shape,
527+
t, resample=True)
528+
# Resample and norm data
532529
if self.cmap.n_variates == 1:
533-
self.nac._norm[0].autoscale_None(A)
534-
A = self.to_rgba(A)
530+
normed_resampled = self._resample_and_norm(A,
531+
self.nac._norm[0],
532+
out_shape,
533+
out_mask, t)
535534
else:
536-
A = cm._iterable_variates_in_data(A)
537-
for n, a in zip(self.nac._norm, A):
538-
n.autoscale_None(a)
539-
# We could invoked A = self.to_rgba(A) here
540-
# but that would result in an extra memcopy.
541-
# Instead do norm + cmap() directly.
542-
A = [norm(xx) for norm, xx in zip(self.nac._norm, A)]
543-
A = self.cmap(A)
544-
alpha = self._get_scalar_alpha()
535+
normed_resampled = [self._resample_and_norm(a,
536+
self.nac._norm[i],
537+
out_shape,
538+
out_mask, t)
539+
for i, a in
540+
enumerate(cm._iterable_variates_in_data(A))]
541+
output = self.cmap(normed_resampled, bytes=True)
542+
# Apply alpha *after* if the input was greyscale without a mask
543+
alpha_channel = output[:, :, 3]
544+
alpha_channel[:] = ( # Assignment will cast to uint8.
545+
alpha_channel.astype(np.float32) * out_alpha * scalar_alpha)
546+
547+
else: # A.ndim == 3
545548
if A.shape[2] == 3:
546549
# No need to resample alpha or make a full array; NumPy will expand
547550
# this out and cast to uint8 if necessary when it's assigned to the
548551
# alpha channel below.
549-
output_alpha = (255 * alpha) if A.dtype == np.uint8 else alpha
552+
output_alpha = (255 * scalar_alpha) if A.dtype == np.uint8 \
553+
else scalar_alpha
550554
else:
551555
output_alpha = _resample( # resample alpha channel
552-
self, A[..., 3], out_shape, t, alpha=alpha)
556+
self, A[..., 3], out_shape, t, alpha=scalar_alpha)
557+
553558
output = _resample( # resample rgb channels
554-
self, _rgb_to_rgba(A[..., :3]), out_shape, t, alpha=alpha)
559+
self, _rgb_to_rgba(A[..., :3]), out_shape, t, alpha=scalar_alpha)
555560
output[..., 3] = output_alpha # recombine rgb and alpha
556-
557-
# output is now either a 2D array of normed (int or float) data
558-
# or an RGBA array of re-sampled input
559-
output = self.to_rgba(output, bytes=True, norm=False)
561+
if output.dtype.kind == 'f':
562+
output = (output * 255).astype(np.uint8)
560563
# output is now a correctly sized RGBA array of uint8
561-
562-
# Apply alpha *after* if the input was greyscale without a mask
563-
if A.ndim == 2:
564-
alpha = self._get_scalar_alpha()
565-
alpha_channel = output[:, :, 3]
566-
alpha_channel[:] = ( # Assignment will cast to uint8.
567-
alpha_channel.astype(np.float32) * out_alpha * alpha)
568-
569< 538E /code>-
else:
564+
else: # if unsampled:
570565
if self._imcache is None:
571566
self._imcache = self.to_rgba(A, bytes=True, norm=(A.ndim == 2))
572567
output = self._imcache

0 commit comments

Comments
 (0)
0