8000 FIX: first pass at re-working image interpolation · matplotlib/matplotlib@0ed8467 · GitHub
[go: up one dir, main page]

Skip to content
8000

Commit 0ed8467

Browse files
committed
FIX: first pass at re-working image interpolation
- interpolate raw, not normed, data - should reduce memory footprint, only 1 or 2 copies of input data - this changes many tests in small ways
1 parent 3bf8dcc commit 0ed8467

File tree

1 file changed

+95
-88
lines changed

1 file changed

+95
-88
lines changed

lib/matplotlib/image.py

Lines changed: 95 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -356,58 +356,96 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
356356
out_height = int(out_height_base)
357357

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

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

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

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

466472
# Apply alpha *after* if the input was greyscale without a mask
467-
if A.ndim == 2 or created_rgba_mask:
473+
if A.ndim == 2:
468474
alpha = self.get_alpha()
469475
if alpha is not None and alpha != 1.0:
470476
alpha_channel = output[:, :, 3]
471477
alpha_channel[:] = np.asarray(
472478
np.asarray(alpha_channel, np.float32) * alpha,
473479
np.uint8)
480+
474481
else:
475482
if self._imcache is None:
476483
self._imcache = self.to_rgba(A, bytes=True, norm=(A.ndim == 2))

0 commit comments

Comments
 (0)
0