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

Skip to content
8000
< 8000 style data-styled="true" data-styled-version="5.3.11">.hEHvLI{min-width:0;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}/*!sc*/ .bmcJak{min-width:0;}/*!sc*/ .fyKNMY[data-size="medium"]{color:var(--fgColor-default,var(--color-fg-default,#1F2328));}/*!sc*/ .gUkoLg{-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;}/*!sc*/ .gLSgdJ{font-weight:600;color:var(--fgColor-default,var(--color-fg-default,#1F2328));}/*!sc*/ .gLSgdJ:hover{color:var(--fgColor-default,var(--color-fg-default,#1F2328));}/*!sc*/ .irPhWZ{width:60px;}/*!sc*/ .dNbsEP{width:62px;}/*!sc*/ .kHfwUD{width:60px;height:22px;}/*!sc*/ .bHLmSv{position:absolute;inset:0 -2px;cursor:col-resize;background-color:transparent;-webkit-transition-delay:0.1s;transition-delay:0.1s;}/*!sc*/ .bHLmSv:hover{background-color:var(--bgColor-neutral-muted,var(--color-neutral-muted,rgba(175,184,193,0.2)));}/*!sc*/ .iuodyc{border-bottom-left-radius:6px;border-bottom-right-radius:6px;line-height:0;padding:32px;text-align:center;}/*!sc*/ .bBZNhw{height:1px;width:1px;position:fixed;bottom:0;right:0;}/*!sc*/ .hqtbbn{bottom:0 !important;-webkit-clip:rect(1px,1px,1px,1px);clip:rect(1px,1px,1px,1px);-webkit-clip-path:inset(50%);clip-path:inset(50%);height:84px;position:absolute;width:320px;}/*!sc*/ data-styled.g1[id="Box-sc-g0xbh4-0"]{content:"hEHvLI,bmcJak,fyKNMY,gUkoLg,gLSgdJ,irPhWZ,dNbsEP,kHfwUD,bHLmSv,iuodyc,bBZNhw,hqtbbn,"}/*!sc*/ .brGdpi{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;-webkit-clip:rect(0,0,0,0);clip:rect(0,0,0,0);white-space:nowrap;border-width:0;}/*!sc*/ data-styled.g3[id="_VisuallyHidden__VisuallyHidden-sc-11jhm7a-0"]{content:"brGdpi,"}/*!sc*/ .jjwhNb{position:relative;display:inline-block;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}/*!sc*/ .jjwhNb::after{position:absolute;z-index:1000000;display:none;padding:0.5em 0.75em;font:normal normal 11px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";-webkit-font-smoothing:subpixel-antialiased;color:var(--tooltip-fgColor,var(--fgColor-onEmphasis,var(--color-fg-on-emphasis,#ffffff)));text-align:center;-webkit-text-decoration:none;text-decoration:none;text-shadow:none;text-transform:none;-webkit-letter-spacing:normal;-moz-letter-spacing:normal;-ms-letter-spacing:normal;letter-spacing:normal;word-wrap:break-word;white-space:pre;pointer-events:none;content:attr(aria-label);background:var(--tooltip-bgColor,var(--bgColor-emphasis,var(--color-neutral-emphasis-plus,#24292f)));border-radius:6px;opacity:0;}/*!sc*/ @-webkit-keyframes tooltip-appear{from{opacity:0;}to{opacity:1;}}/*!sc*/ @keyframes tooltip-appear{from{opacity:0;}to{opacity:1;}}/*!sc*/ .jjwhNb:hover::after,.jjwhNb:active::after,.jjwhNb:focus::after,.jjwhNb:focus-within::after{display:inline-block;-webkit-text-decoration:none;text-decoration:none;-webkit-animation-name:tooltip-appear;animation-name:tooltip-appear;-webkit-animation-duration:0.1s;animation-duration:0.1s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;-webkit-animation-delay:0s;animation-delay:0s;}/*!sc*/ .jjwhNb.tooltipped-no-delay:hover::after,.jjwhNb.tooltipped-no-delay:active::after,.jjwhNb.tooltipped-no-delay:focus::after,.jjwhNb.tooltipped-no-delay:focus-within::after{-webkit-animation-delay:0s;animation-delay:0s;}/*!sc*/ .jjwhNb.tooltipped-multiline:hover::after,.jjwhNb.tooltipped-multiline:active::after,.jjwhNb.tooltipped-multiline:focus::after,.jjwhNb.tooltipped-multiline:focus-within::after{display:table-cell;}/*!sc*/ .jjwhNb.tooltipped-s::after,.jjwhNb.tooltipped-se::after,.jjwhNb.tooltipped-sw::after{top:100%;right:50%;margin-top:6px;}/*!sc*/ .jjwhNb.tooltipped-se::after{right:auto;left:50%;margin-left:-16px;}/*!sc*/ .jjwhNb.tooltipped-sw::after{margin-right:-16px;}/*!sc*/ .jjwhNb.tooltipped-n::after,.jjwhNb.tooltipped-ne::after,.jjwhNb.tooltipped-nw::after{right:50%;bottom:100%;margin-bottom:6px;}/*!sc*/ .jjwhNb.tooltipped-ne::after{right:auto;left:50%;margin-left:-16px;}/*!sc*/ .jjwhNb.tooltipped-nw::after{margin-right:-16px;}/*!sc*/ .jjwhNb.tooltipped-s::after,.jjwhNb.tooltipped-n::after{-webkit-transform:translateX(50%);-ms-transform:translateX(50%);transform:translateX(50%);}/*!sc*/ .jjwhNb.tooltipped-w::after{right:100%;bottom:50%;margin-right:6px;-webkit-transform:translateY(50%);-ms-transform:translateY(50%);transform:translateY(50%);}/*!sc*/ .jjwhNb.tooltipped-e::after{bottom:50%;left:100%;margin-left:6px;-webkit-transform:translateY(50%);-ms-transform:translateY(50%);transform:translateY(50%);}/*!sc*/ .jjwhNb.tooltipped-multiline::after{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:250px;word-wrap:break-word;white-space:pre-line;border-collapse:separate;}/*!sc*/ .jjwhNb.tooltipped-multiline.tooltipped-s::after,.jjwhNb.tooltipped-multiline.tooltipped-n::after{right:auto;left:50%;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%);}/*!sc*/ .jjwhNb.tooltipped-multiline.tooltipped-w::after,.jjwhNb.tooltipped-multiline.tooltipped-e::after{right:100%;}/*!sc*/ .jjwhNb.tooltipped-align-right-2::after{right:0;margin-right:0;}/*!sc*/ .jjwhNb.tooltipped-align-left-2::after{left:0;margin-left:0;}/*!sc*/ data-styled.g6[id="Tooltip__TooltipBase-sc-17tf59c-0"]{content:"jjwhNb,"}/*!sc*/ .irithh{position:relative;overflow:hidden;-webkit-mask-image:radial-gradient(white,black);mask-image:radial-gradient(white,black);background-color:var(--bgColor-neutral-muted,var(--color-neutral-subtle,rgba(234,238,242,0.5)));border-radius:3px;display:block;height:1.2em;width:60px;}/*!sc*/ .irithh::after{-webkit-animation:crVFvv 1.5s infinite linear;animation:crVFvv 1.5s infinite linear;background:linear-gradient(90deg,transparent,var(--bgColor-neutral-muted,var(--color-neutral-subtle,rgba(234,238,242,0.5))),transparent);content:'';position:absolute;-webkit-transform:translateX(-100%);-ms-transform:translateX(-100%);transform:translateX(-100%);bottom:0;left:0;right:0;top:0;}/*!sc*/ .ihfxfT{position:relative;overflow:hidden;-webkit-mask-image:radial-gradient(white,black);mask-image:radial-gradient(white,black);background-color:var(--bgColor-neutral-muted,var(--color-neutral-subtle,rgba(234,238,242,0.5)));border-radius:3px;display:block;height:1.2em;width:62px;}/*!sc*/ .ihfxfT::after{-webkit-animation:crVFvv 1.5s infinite linear;animation:crVFvv 1.5s infinite linear;background:linear-gradient(90deg,transparent,var(--bgColor-neutral-muted,var(--color-neutral-subtle,rgba(234,238,242,0.5))),transparent);content:'';position:absolute;-webkit-transform:translateX(-100%);-ms-transform:translateX(-100%);transform:translateX(-100%);bottom:0;left:0;right:0;top:0;}/*!sc*/ .kRBfod{position:relative;overflow:hidden;-webkit-mask-image:radial-gradient(white,black);mask-image:radial-gradient(white,black);background-color:var(--bgColor-neutral-muted,var(--color-neutral-subtle,rgba(234,238,242,0.5)));border-radius:3px;display:block;height:1.2em;width:60px;height:22px;}/*!sc*/ .kRBfod::after{-webkit-animation:crVFvv 1.5s infinite linear;animation:crVFvv 1.5s infinite linear;background:linear-gradient(90deg,transparent,var(--bgColor-neutral-muted,var(--color-neutral-subtle,rgba(234,238,242,0.5))),transparent);content:'';position:absolute;-webkit-transform:translateX(-100%);-ms-transform:translateX(-100%);transform:translateX(-100%);bottom:0;left:0;right:0;top:0;}/*!sc*/ data-styled.g27[id="LoadingSkeleton-sc-695d630a-0"]{content:"irithh,ihfxfT,kRBfod,"}/*!sc*/ @-webkit-keyframes crVFvv{0%{-webkit-transform:translateX(-100%);-ms-transform:translateX(-100%);transform:translateX(-100%);}50%{-webkit-transform:translateX(100%);-ms-transform:translateX(100%);transform:translateX(100%);}100%{-webkit-transform:translateX(100%);-ms-transform:translateX(100%);transform:translateX(100%);}}/*!sc*/ @keyframes crVFvv{0%{-webkit-transform:translateX(-100%);-ms-transform:translateX(-100%);transform:translateX(-100%);}50%{-webkit-transform:translateX(100%);-ms-transform:translateX(100%);transform:translateX(100%);}100%{-webkit-transform:translateX(100%);-ms-transform:translateX(100%);transform:translateX(100%);}}/*!sc*/ data-styled.g54[id="sc-keyframes-crVFvv"]{content:"crVFvv,"}/*!sc*/

Commit 2db0d6a

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 5f534ad commit 2db0d6a

File tree

5 files changed

+165
-158
lines changed

5 files changed

+165
-158
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< 8000 /span>.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