Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 2db0d6a

Browse filesBrowse 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 5f534ad commit 2db0d6a
Copy full SHA for 2db0d6a

File tree

5 files changed

+165
-158
lines changed
Filter options

5 files changed

+165
-158
lines changed

‎lib/matplotlib/image.py

Copy file name to clipboardExpand all lines: lib/matplotlib/image.py
+95-88Lines 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))
Binary file not shown.
Loading

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.