Skip to content

Commit b1b9404

Browse files
authored
Merge pull request #8966 from tacaswell/fix_image_interpolation
Fix image interpolation
2 parents 6c69f3e + 1aebff1 commit b1b9404

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+2349
-2702
lines changed

lib/matplotlib/cm.py

+1
Original file line numberDiff line numberDiff line change
@@ -375,3 +375,4 @@ def changed(self):
375375

376376
for key in self.update_dict:
377377
self.update_dict[key] = True
378+
self.stale = True

lib/matplotlib/image.py

+105-101
Original file line numberDiff line numberDiff line change
@@ -192,14 +192,6 @@ def _interpdr(self):
192192
def iterpnames(self):
193193
return interpolations_names
194194

195-
def set_cmap(self, cmap):
196-
super(_ImageBase, self).set_cmap(cmap)
197-
self.stale = True
198-
199-
def set_norm(self, norm):
200-
super(_ImageBase, self).set_norm(norm)
201-
self.stale = True
202-
203195
def __str__(self):
204196
return "AxesImage(%g,%g;%gx%g)" % tuple(self.axes.bbox.bounds)
205197

@@ -357,58 +349,100 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
357349
out_height = int(out_height_base)
358350

359351
if not unsampled:
360-
created_rgba_mask = False
361-
362352
if A.ndim not in (2, 3):
363353
raise ValueError("Invalid dimensions, got {}".format(A.shape))
364354

365355
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
356+
# if we are a 2D array, then we are running through the
357+
# norm + colormap transformation. However, in general the
358+
# input data is not going to match the size on the screen so we
359+
# have to resample to the correct number of pixels
360+
# need to
361+
362+
# TODO slice input array first
363+
inp_dtype = A.dtype
364+
if inp_dtype.kind == 'f':
365+
scaled_dtype = A.dtype
366+
else:
367+
scaled_dtype = np.float32
368+
# old versions of numpy do not work with `np.nammin`
369+
# and `np.nanmax` as inputs
370+
a_min = np.ma.min(A)
371+
a_max = np.ma.max(A)
372+
# scale the input data to [.1, .9]. The Agg
373+
# interpolators clip to [0, 1] internally, use a
374+
# smaller input scale to identify which of the
375+
# interpolated points need to be should be flagged as
376+
# over / under.
377+
# This may introduce numeric instabilities in very broadly
378+
# scaled data
379+
A_scaled = np.empty(A.shape, dtype=scaled_dtype)
380+
A_scaled[:] = A
381+
A_scaled -= a_min
382+
if a_min != a_max:
383+
A_scaled /= ((a_max - a_min) / 0.8)
384+
A_scaled += 0.1
385+
A_resampled = np.zeros((out_height, out_width),
386+
dtype=A_scaled.dtype)
387+
# resample the input data to the correct resolution and shape
388+
_image.resample(A_scaled, A_resampled,
389+
t,
390+
_interpd_[self.get_interpolation()],
391+
self.get_resample(), 1.0,
392+
self.get_filternorm() or 0.0,
393+
self.get_filterrad() or 0.0)
394+
395+
# we are done with A_scaled now, remove from namespace
396+
# to be sure!
397+
del A_scaled
398+
# un-scale the resampled data to approximately the
399+
# original range things that interpolated to above /
400+
# below the original min/max will still be above /
401+
# below, but possibly clipped in the case of higher order
402+
# interpolation + drastically changing data.
403+
A_resampled -= 0.1
404+
if a_min != a_max:
405+
A_resampled *= ((a_max - a_min) / 0.8)
406+
A_resampled += a_min
407+
# if using NoNorm, cast back to the original datatype
408+
if isinstance(self.norm, mcolors.NoNorm):
409+
A_resampled = A_resampled.astype(A.dtype)
410+
411+
mask = np.empty(A.shape, dtype=np.float32)
412+
if A.mask.shape == A.shape:
413+
# this is the case of a nontrivial mask
414+
mask[:] = np.where(A.mask, np.float32(np.nan),
415+
np.float32(1))
398416
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:
417+
mask[:] = 1
418+
419+
# we always have to interpolate the mask to account for
420+
# non-affine transformations
421+
out_mask = np.zeros((out_height, out_width),
422+
dtype=mask.dtype)
423+
_image.resample(mask, out_mask,
424+
t,
425+
_interpd_[self.get_interpolation()],
426+
True, 1,
427+
self.get_filternorm() or 0.0,
428+
self.get_filterrad() or 0.0)
429+
# we are done with the mask, delete from namespace to be sure!
430+
del mask
431+
# Agg updates the out_mask in place. If the pixel has
432+
# no image data it will not be updated (and still be 0
433+
# as we initialized it), if input data that would go
434+
# into that output pixel than it will be `nan`, if all
435+
# the input data for a pixel is good it will be 1, and
436+
# if there is _some_ good data in that output pixel it
437+
# will be between [0, 1] (such as a rotated image).
438+
439+
out_alpha = np.array(out_mask)
440+
out_mask = np.isnan(out_mask)
441+
out_alpha[out_mask] = 1
442+
443+
# mask and run through the norm
444+
output = self.norm(np.ma.masked_array(A_resampled, out_mask))
445+
else:
412446
# Always convert to RGBA, even if only RGB input
413447
if A.shape[2] == 3:
414448
A = _rgb_to_rgba(A)
@@ -421,57 +455,27 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
421455
if alpha is None:
422456
alpha = 1.0
423457

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
458+
_image.resample(
459+
A, output, t, _interpd_[self.get_interpolation()],
460+
self.get_resample(), alpha,
461+
self.get_filternorm() or 0.0, self.get_filterrad() or 0.0)
464462

463+
# at this point output is either a 2D array of normed data
464+
# (of int or float)
465+
# or an RGBA array of re-sampled input
465466
output = self.to_rgba(output, bytes=True, norm=False)
467+
# output is now a correctly sized RGBA array of uint8
466468

467469
# Apply alpha *after* if the input was greyscale without a mask
468-
if A.ndim == 2 or created_rgba_mask:
470+
if A.ndim == 2:
469471
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)
472+
if alpha is None:
473+
alpha = 1
474+
alpha_channel = output[:, :, 3]
475+
alpha_channel[:] = np.asarray(
476+
np.asarray(alpha_channel, np.float32) * out_alpha * alpha,
477+
np.uint8)
478+
475479
else:
476480
if self._imcache is None:
477481
self._imcache = self.to_rgba(A, bytes=True, norm=(A.ndim == 2))
Binary file not shown.
Loading

lib/matplotlib/tests/baseline_images/test_axes/imshow.svg

+56-116
Loading
Binary file not shown.

lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.svg

+407-471
Loading
Binary file not shown.

lib/matplotlib/tests/baseline_images/test_image/bbox_image_inverted.svg

+60-130
Loading
Binary file not shown.

lib/matplotlib/tests/baseline_images/test_image/image_clip.svg

+254-135
Loading
Binary file not shown.

lib/matplotlib/tests/baseline_images/test_image/image_cliprect.svg

+76-142
Loading
Binary file not shown.

0 commit comments

Comments
 (0)