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

Skip to content

Commit 12c27f3

Browse files
committed
FIX: re-work image interpolation
- interpolate raw, not normed, data - should reduce memory footprint, only 1 or 2 copies of input data - this will change many tests in small ways - down-casting input data before interpolation was causing issues with numerical precision so work in no less than the input float type - Numpy 1.7 does not support `np.nanmin` and `np.nanmax` on masked arrays (instead of returning a number it returns a MaskedIterator).
1 parent cb7b60f commit 12c27f3

File tree

1 file changed

+103
-93
lines changed

1 file changed

+103
-93
lines changed

lib/matplotlib/image.py

Lines changed: 103 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -357,58 +357,98 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
357357
out_height = int(out_height_base)
358358

359359
if not unsampled:
360-
created_rgba_mask = False
361-
362360
if A.ndim not in (2, 3):
363361
raise ValueError("Invalid dimensions, got {}".format(A.shape))
364362

365363
if A.ndim == 2:
366-
A = self.norm(A)
367-
if A.dtype.kind == 'f':
368-
# If the image is greyscale, convert to RGBA and
369-
# use the extra channels for resizing the over,
370-
# under, and bad pixels. This is needed because
371-
# Agg's resampler is very aggressive about
372-
# clipping to [0, 1] and we use out-of-bounds
373-
# values to carry the over/under/bad information
374-
rgba = np.empty((A.shape[0], A.shape[1], 4), dtype=A.dtype)
375-
rgba[..., 0] = A # normalized data
376-
# this is to work around spurious warnings coming
377-
# out of masked arrays.
378-
with np.errstate(invalid='ignore'):
379-
rgba[..., 1] = np.where(A < 0, np.nan, 1) # under data
380-
rgba[..., 2] = np.where(A > 1, np.nan, 1) # over data
381-
# Have to invert mask, Agg knows what alpha means
382-
# so if you put this in as 0 for 'good' points, they
383-
# all get zeroed out
384-
rgba[..., 3] = 1
385-
if A.mask.shape == A.shape:
386-
# this is the case of a nontrivial mask
387-
mask = np.where(A.mask, np.nan, 1)
388-
else:
389-
# this is the case that the mask is a
390-
# numpy.bool_ of False
391-
mask = A.mask
392-
# ~A.mask # masked data
393-
A = rgba
394-
output = np.zeros((out_height, out_width, 4),
395-
dtype=A.dtype)
396-
alpha = 1.0
397-
created_rgba_mask = True
364+
# if we are a 2D array, then we are running through the
365+
# norm + colormap transformation. However, in general the
366+
# input data is not going to match the size on the screen so we
367+
# have to resample to the correct number of pixels
368+
# need to
369+
370+
# TODO slice input array first
371+
inp_dtype = A.dtype
372+
if inp_dtype.kind == 'f':
373+
scaled_dtype = A.dtype
398374
else:
399-
# colormap norms that output integers (ex NoNorm
400-
# and BoundaryNorm) to RGBA space before
401-
# interpolating. This is needed due to the
402-
# Agg resampler only working on floats in the
403-
# range [0, 1] and because interpolating indexes
404-
# into an arbitrary LUT may be problematic.
405-
#
406-
# This falls back to interpolating in RGBA space which
407-
# can produce it's own artifacts of colors not in the map
408-
# showing up in the final image.
409-
A = self.cmap(A, alpha=self.get_alpha(), bytes=True)
410-
411-
if not created_rgba_mask:
375+
scaled_dtype = np.float32
376+
# old versions of numpy do not work with `np.nammin`
377+
# and `np.nanmax` as inputs
378+
a_min = np.ma.min(A)
379+
a_max = np.ma.max(A)
380+
# scale the input data to [.1, .9]. The Agg
381+
# interpolators clip to [0, 1] internally, use a
382+
# smaller input scale to identify which of the
383+
# interpolated points need to be should be flagged as
384+
# over / under.
385+
# This may introduce numeric instabilities in very broadly
386+
# scaled data
387+
A_scaled = np.empty(A.shape, dtype=scaled_dtype)
388+
A_scaled[:] = A
389+
A_scaled -= a_min
390+
A_scaled /= ((a_max - a_min) / 0.8)
391+
A_scaled += 0.1
392+
A_resampled = np.zeros((out_height, out_width),
393+
dtype=A_scaled.dtype)
394+
# resample the input data to the correct resolution and shape
395+
_image.resample(A_scaled, A_resampled,
396+
t,
397+
_interpd_[self.get_interpolation()],
398+
self.get_resample(), 1.0,
399+
self.get_filternorm() or 0.0,
400+
self.get_filterrad() or 0.0)
401+
402+
# we are done with A_scaled now, remove from namespace
403+
# to be sure!
404+
del A_scaled
405+
# un-scale the resampled data to approximately the
406+
# original range things that interpolated to above /
407+
# below the original min/max will still be above /
408+
# below, but possibly clipped in the case of higher order
409+
# interpolation + drastically changing data.
410+
A_resampled -= 0.1
411+
A_resampled *= ((a_max - a_min) / 0.8)
412+
A_resampled += a_min
413+
# if using NoNorm, cast back to the original datatype
414+
if isinstance(self.norm, mcolors.NoNorm):
415+
A_resampled = A_resampled.astype(A.dtype)
416+
417+
mask = np.empty(A.shape, dtype=np.float32)
418+
if A.mask.shape == A.shape:
419+
# this is the case of a nontrivial mask
420+
mask[:] = np.where(A.mask, np.float32(np.nan),
421+
np.float32(1))
422+
else:
423+
mask[:] = 1
424+
425+
# we always have to interpolate the mask to account for
426+
# non-affine transformations
427+
out_mask = np.zeros((out_height, out_width),
428+
dtype=mask.dtype)
429+
_image.resample(mask, out_mask,
430+
t,
431+
_interpd_[self.get_interpolation()],
432+
True, 1,
433+
self.get_filternorm() or 0.0,
434+
self.get_filterrad() or 0.0)
435+
# we are done with the mask, delete from namespace to be sure!
436+
del mask
437+
# Agg updates the out_mask in place. If the pixel has
438+
# no image data it will not be updated (and still be 0
439+
# as we initialized it), if input data that would go
440+
# into that output pixel than it will be `nan`, if all
441+
# the input data for a pixel is good it will be 1, and
442+
# if there is _some_ good data in that output pixel it
443+
# will be between [0, 1] (such as a rotated image).
444+
445+
out_alpha = np.array(out_mask)
446+
out_mask = np.isnan(out_mask)
447+
out_alpha[out_mask] = 1
448+
449+
# mask and run through the norm
450+
output = self.norm(np.ma.masked_array(A_resampled, out_mask))
451+
else:
412452
# Always convert to RGBA, even if only RGB input
413453
if A.shape[2] == 3:
414454
A = _rgb_to_rgba(A)
@@ -421,57 +461,27 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
421461
if alpha is None:
422462
alpha = 1.0
423463

424-
_image.resample(
425-
A, output, t, _interpd_[self.get_interpolation()],
426-
self.get_resample(), alpha,
427-
self.get_filternorm() or 0.0, self.get_filterrad() or 0.0)
428-
429-
if created_rgba_mask:
430-
# Convert back to a masked greyscale array so
431-
# colormapping works correctly
432-
hid_output = output
433-
# any pixel where the a masked pixel is included
434-
# in the kernel (pulling this down from 1) needs to
435-
# be masked in the output
436-
if len(mask.shape) == 2:
437-
out_mask = np.empty((out_height, out_width),
438-
dtype=mask.dtype)
439-
_image.resample(mask, out_mask, t,
440-
_interpd_[self.get_interpolation()],
441-
True, 1,
442-
self.get_filternorm() or 0.0,
443-
self.get_filterrad() or 0.0)
444-
out_mask = np.isnan(out_mask)
445-
else:
446-
out_mask = mask
447-
# we need to mask both pixels which came in as masked
448-
# and the pixels that Agg is telling us to ignore (relavent
449-
# to non-affine transforms)
450-
# Use half alpha as the threshold for pixels to mask.
451-
out_mask = out_mask | (hid_output[..., 3] < .5)
452-
output = np.ma.masked_array(
453-
hid_output[..., 0],
454-
out_mask)
455-
# 'unshare' the mask array to
456-
# needed to suppress numpy warning
457-
del out_mask
458-
invalid_mask = ~output.mask * ~np.isnan(output.data)
459-
# relabel under data. If any of the input data for
460-
# the pixel has input out of the norm bounds,
461-
output[np.isnan(hid_output[..., 1]) * invalid_mask] = -1
462-
# relabel over data
463-
output[np.isnan(hid_output[..., 2]) * invalid_mask] = 2
464+
_image.resample(
465+
A, output, t, _interpd_[self.get_interpolation()],
466+
self.get_resample(), alpha,
467+
self.get_filternorm() or 0.0, self.get_filterrad() or 0.0)
464468

469+
# at this point output is either a 2D array of normed data
470+
# (of int or float)
471+
# or an RGBA array of re-sampled input
465472
output = self.to_rgba(output, bytes=True, norm=False)
473+
# output is now a correctly sized RGBA array of uint8
466474

467475
# Apply alpha *after* if the input was greyscale without a mask
468-
if A.ndim == 2 or created_rgba_mask:
476+
if A.ndim == 2:
469477
alpha = self.get_alpha()
470-
if alpha is not None and alpha != 1.0:
471-
alpha_channel = output[:, :, 3]
472-
alpha_channel[:] = np.asarray(
473-
np.asarray(alpha_channel, np.float32) * alpha,
474-
np.uint8)
478+
if alpha is None:
479+
alpha = 1
480+
alpha_channel = output[:, :, 3]
481+
alpha_channel[:] = np.asarray(
482+
np.asarray(alpha_channel, np.float32) * out_alpha * alpha,
483+
np.uint8)
484+
475485
else:
476486
if self._imcache is None:
477487
self._imcache = self.to_rgba(A, bytes=True, norm=(A.ndim == 2))

0 commit comments

Comments
 (0)