Skip to content

Commit 6595139

Browse files
committed
Merge pull request #5718 from mdboom/image-interpolation
Rewrite of image infrastructure Conflicts: lib/matplotlib/tests/test_axes.py - do not back-port appveyor spceific limits lib/matplotlib/tests/test_image.py - do not backport the jpeg_alpha test setupext.py - do not include windows/appveyor related changes
1 parent c27dd78 commit 6595139

File tree

152 files changed

+13992
-12746
lines changed

Some content is hidden

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

152 files changed

+13992
-12746
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Change in the ``draw_image`` backend API
2+
----------------------------------------
3+
4+
The ``draw_image`` method implemented by backends has changed its interface.
5+
6+
This change is only relevant if the backend declares that it is able
7+
to transform images by returning ``True`` from ``option_scale_image``.
8+
See the ``draw_image`` docstring for more information.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
Improved image support
2+
----------------------
3+
4+
Prior to version 2.0, matplotlib resampled images by first applying
5+
the color map and then resizing the result. Since the resampling was
6+
performed on the colored image, this introduced colors in the output
7+
image that didn't actually exist in the color map. Now, images are
8+
resampled first (and entirely in floating-point, if the input image is
9+
floating-point), and then the color map is applied.
10+
11+
In order to make this important change, the image handling code was
12+
almost entirely rewritten. As a side effect, image resampling uses
13+
less memory and fewer datatype conversions than before.
14+
15+
The experimental private feature where one could "skew" an image by
16+
setting the private member ``_image_skew_coordinate`` has been
17+
removed. Instead, images will obey the transform of the axes on which
18+
they are drawn.

doc/users/whats_new/rcparams.rst

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,20 @@ Configuration (rcParams)
1919
|`svg.hashsalt` | see note |
2020
+----------------------------+--------------------------------------------------+
2121

22-
``svg.hashsalt``
23-
````````````````
22+
Added ``svg.hashsalt`` key to rcParams
23+
```````````````````````````````````````
2424

2525
If ``svg.hashsalt`` is ``None`` (which it is by default), the svg
2626
backend uses ``uuid4`` to generate the hash salt. If it is not
2727
``None``, it must be a string that is used as the hash salt instead of
2828
``uuid4``. This allows for deterministic SVG output.
29+
30+
31+
Removed the ``svg.image_noscale`` rcParam
32+
`````````````````````````````````````````
33+
34+
As a result of the extensive changes to image handling, the
35+
``svg.image_noscale`` rcParam has been removed. The same
36+
functionality may be achieved by setting ``interpolation='none'`` on
37+
individual images or globally using the ``image.interpolation``
38+
rcParam.

examples/api/demo_affine_image.py

100755100644
Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
#!/usr/bin/env python
2-
3-
41
"""
52
For the backends that supports draw_image with optional affine
63
transform (e.g., agg, ps backend), the image of the output should
@@ -24,22 +21,15 @@ def get_image():
2421
return Z
2522

2623

27-
def imshow_affine(ax, z, *kl, **kwargs):
28-
im = ax.imshow(z, *kl, **kwargs)
29-
x1, x2, y1, y2 = im.get_extent()
30-
im._image_skew_coordinate = (x2, y1)
31-
return im
32-
33-
3424
if 1:
3525

3626
# image rotation
3727

38-
fig, (ax1, ax2) = plt.subplots(1, 2)
28+
fig, ax1 = plt.subplots(1, 1)
3929
Z = get_image()
40-
im1 = imshow_affine(ax1, Z, interpolation='none',
41-
origin='lower',
42-
extent=[-2, 4, -3, 2], clip_on=True)
30+
im1 = ax1.imshow(Z, interpolation='none',
31+
origin='lower',
32+
extent=[-2, 4, -3, 2], clip_on=True)
4333

4434
trans_data2 = mtransforms.Affine2D().rotate_deg(30) + ax1.transData
4535
im1.set_transform(trans_data2)
@@ -53,13 +43,3 @@ def imshow_affine(ax, z, *kl, **kwargs):
5343

5444
ax1.set_xlim(-3, 5)
5545
ax1.set_ylim(-4, 4)
56-
57-
# image skew
58-
59-
im2 = ax2.imshow(Z, interpolation='none',
60-
origin='lower',
61-
extent=[-2, 4, -3, 2], clip_on=True)
62-
im2._image_skew_coordinate = (3, -2)
63-
64-
plt.show()
65-
#plt.savefig("demo_affine_image")

examples/images_contours_and_fields/interpolation_methods.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
'spline36', 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric',
1919
'catrom', 'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos']
2020

21+
np.random.seed(0)
2122
grid = np.random.rand(4, 4)
2223

2324
fig, axes = plt.subplots(3, 6, figsize=(12, 6),
@@ -26,7 +27,7 @@
2627
fig.subplots_adjust(hspace=0.3, wspace=0.05)
2728

2829
for ax, interp_method in zip(axes.flat, methods):
29-
ax.imshow(grid, interpolation=interp_method)
30+
ax.imshow(grid, interpolation=interp_method, cmap='viridis')
3031
ax.set_title(interp_method)
3132

3233
plt.show()

examples/pylab_examples/demo_annotation_box.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848

4949
arr = np.arange(100).reshape((10, 10))
5050
im = OffsetImage(arr, zoom=2)
51+
im.image.axes = ax
5152

5253
ab = AnnotationBbox(im, xy,
5354
xybox=(-50., 50.),
@@ -62,9 +63,10 @@
6263

6364
from matplotlib._png import read_png
6465
fn = get_sample_data("grace_hopper.png", asfileobj=False)
65-
arr_lena = read_png(fn)
66+
arr_img = read_png(fn)
6667

67-
imagebox = OffsetImage(arr_lena, zoom=0.2)
68+
imagebox = OffsetImage(arr_img, zoom=0.2)
69+
imagebox.image.axes = ax
6870

6971
ab = AnnotationBbox(imagebox, xy,
7072
xybox=(120., -80.),

extern/agg24-svn/include/agg_span_image_filter_gray.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ namespace agg
490490
fg_ptr = (const value_type*)base_type::source().next_y();
491491
}
492492

493-
fg >>= image_filter_shift;
493+
fg = color_type::downshift(fg, image_filter_shift);
494494
if(fg < 0) fg = 0;
495495
if(fg > color_type::full_value()) fg = color_type::full_value();
496496
span->v = (value_type)fg;

lib/matplotlib/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,7 @@ def matplotlib_fname():
842842
'savefig.extension': ('savefig.format', lambda x: x, None),
843843
'axes.color_cycle': ('axes.prop_cycle', lambda x: cycler('color', x),
844844
lambda x: [c.get('color', None) for c in x]),
845+
'svg.image_noscale': ('image.interpolation', None, None),
845846
}
846847

847848
_deprecated_ignore_map = {

lib/matplotlib/axes/_base.py

Lines changed: 1 addition & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2321,15 +2321,6 @@ def draw(self, renderer=None, inframe=False):
23212321
artists.remove(self._left_title)
23222322
artists.remove(self._right_title)
23232323

2324-
# add images to dsu if the backend supports compositing.
2325-
# otherwise, does the manual compositing without adding images to dsu.
2326-
if len(self.images) <= 1 or renderer.option_image_nocomposite():
2327-
_do_composite = False
2328-
else:
2329-
_do_composite = True
2330-
for im in self.images:
2331-
artists.remove(im)
2332-
23332324
if self.figure.canvas.is_saving():
23342325
dsu = [(a.zorder, a) for a in artists]
23352326
else:
@@ -2354,46 +2345,12 @@ def draw(self, renderer=None, inframe=False):
23542345
if self.axison and self._frameon:
23552346
self.patch.draw(renderer)
23562347

2357-
if _do_composite:
2358-
# make a composite image, blending alpha
2359-
# list of (mimage.Image, ox, oy)
2360-
2361-
zorder_images = [(im.zorder, im) for im in self.images
2362-
if im.get_visible()]
2363-
zorder_images.sort(key=lambda x: x[0])
2364-
2365-
mag = renderer.get_image_magnification()
2366-
ims = [(im.make_image(mag), 0, 0, im.get_alpha())
2367-
for z, im in zorder_images]
2368-
2369-
l, b, r, t = self.bbox.extents
2370-
width = int(mag * ((np.round(r) + 0.5) - (np.round(l) - 0.5)))
2371-
height = int(mag * ((np.round(t) + 0.5) - (np.round(b) - 0.5)))
2372-
im = mimage.from_images(height,
2373-
width,
2374-
ims)
2375-
2376-
im.is_grayscale = False
2377-
l, b, w, h = self.bbox.bounds
2378-
# composite images need special args so they will not
2379-
# respect z-order for now
2380-
2381-
gc = renderer.new_gc()
2382-
gc.set_clip_rectangle(self.bbox)
2383-
gc.set_clip_path(mtransforms.TransformedPath(
2384-
self.patch.get_path(),
2385-
self.patch.get_transform()))
2386-
2387-
renderer.draw_image(gc, round(l), round(b), im)
2388-
gc.restore()
2389-
23902348
if dsu_rasterized:
23912349
for zorder, a in dsu_rasterized:
23922350
a.draw(renderer)
23932351
renderer.stop_rasterizing()
23942352

2395-
for zorder, a in dsu:
2396-
a.draw(renderer)
2353+
mimage._draw_list_compositing_images(renderer, self, dsu)
23972354

23982355
renderer.close_group('axes')
23992356
self._cachedRenderer = renderer

lib/matplotlib/backend_bases.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ def get_image_magnification(self):
515515
"""
516516
return 1.0
517517

518-
def draw_image(self, gc, x, y, im):
518+
def draw_image(self, gc, x, y, im, trans=None):
519519
"""
520520
Draw the image instance into the current axes;
521521
@@ -531,7 +531,14 @@ def draw_image(self, gc, x, y, im):
531531
is the distance from bottom
532532
533533
*im*
534-
the :class:`matplotlib._image.Image` instance
534+
An NxMx4 array of RGBA pixels (of dtype uint8).
535+
536+
*trans*
537+
If the concrete backend is written such that
538+
`option_scale_image` returns `True`, an affine
539+
transformation may also be passed to `draw_image`. The
540+
backend should apply the transformation to the image
541+
before applying the translation of `x` and `y`.
535542
"""
536543
raise NotImplementedError
537544

lib/matplotlib/backends/backend_agg.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -318,9 +318,9 @@ def option_image_nocomposite(self):
318318

319319
def option_scale_image(self):
320320
"""
321-
agg backend support arbitrary scaling of image.
321+
agg backend doesn't support arbitrary scaling of image.
322322
"""
323-
return True
323+
return False
324324

325325
def restore_region(self, region, bbox=None, xy=None):
326326
"""
@@ -388,28 +388,25 @@ def post_processing(image, dpi):
388388
# For agg_filter to work, the rendere's method need
389389
# to overridden in the class. See draw_markers, and draw_path_collections
390390

391-
from matplotlib._image import fromarray
392-
393391
width, height = int(self.width), int(self.height)
394392

395393
buffer, bounds = self.tostring_rgba_minimized()
396394

397395
l, b, w, h = bounds
398396

399-
400397
self._renderer = self._filter_renderers.pop()
401398
self._update_methods()
402399

403400
if w > 0 and h > 0:
404401
img = np.fromstring(buffer, np.uint8)
405402
img, ox, oy = post_processing(img.reshape((h, w, 4)) / 255.,
406403
self.dpi)
407-
image = fromarray(img, 1)
408-
409404
gc = self.new_gc()
410-
self._renderer.draw_image(gc,
411-
l+ox, height - b - h +oy,
412-
image)
405+
if img.dtype.kind == 'f':
406+
img = np.asarray(img * 255., np.uint8)
407+
img = img[::-1]
408+
self._renderer.draw_image(
409+
gc, l + ox, height - b - h + oy, img)
413410

414411

415412
def new_figure_manager(num, *args, **kwargs):

lib/matplotlib/backends/backend_cairo.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,14 +171,18 @@ def draw_image(self, gc, x, y, im):
171171
# bbox - not currently used
172172
if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))
173173

174-
rows, cols, buf = im.color_conv (BYTE_FORMAT)
175-
surface = cairo.ImageSurface.create_for_data (
176-
buf, cairo.FORMAT_ARGB32, cols, rows, cols*4)
174+
if sys.byteorder == 'little':
175+
im = im[:, :, (2, 1, 0, 3)]
176+
else:
177+
im = im[:, :, (3, 0, 1, 2)]
178+
surface = cairo.ImageSurface.create_for_data(
179+
memoryview(im.flatten()), cairo.FORMAT_ARGB32, im.shape[1], im.shape[0],
180+
im.shape[1]*4)
177181
ctx = gc.ctx
178-
y = self.height - y - rows
182+
y = self.height - y - im.shape[0]
179183

180184
ctx.save()
181-
ctx.set_source_surface (surface, x, y)
185+
ctx.set_source_surface(surface, float(x), float(y))
182186
if gc.get_alpha() != 1.0:
183187
ctx.paint_with_alpha(gc.get_alpha())
184188
else:

lib/matplotlib/backends/backend_gdk.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,17 +109,14 @@ def draw_image(self, gc, x, y, im):
109109
# int(w), int(h))
110110
# set clip rect?
111111

112-
rows, cols, image_str = im.as_rgba_str()
113-
114-
image_array = np.fromstring(image_str, np.uint8)
115-
image_array.shape = rows, cols, 4
112+
rows, cols = im.shape[:2]
116113

117114
pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB,
118115
has_alpha=True, bits_per_sample=8,
119116
width=cols, height=rows)
120117

121118
array = pixbuf_get_pixels_array(pixbuf)
122-
array[:,:,:] = image_array[::-1]
119+
array[:, :, :] = im[::-1]
123120

124121
gc = self.new_gc()
125122

lib/matplotlib/backends/backend_mixed.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from __future__ import (absolute_import, division, print_function,
22
unicode_literals)
33

4+
import numpy as np
5+
46
from matplotlib.externals import six
57

6-
from matplotlib._image import frombuffer
78
from matplotlib.backends.backend_agg import RendererAgg
89
from matplotlib.tight_bbox import process_figure_for_rasterizing
910

@@ -51,7 +52,7 @@ def __init__(self, figure, width, height, dpi, vector_renderer,
5152
# A reference to the figure is needed as we need to change
5253
# the figure dpi before and after the rasterization. Although
5354
# this looks ugly, I couldn't find a better solution. -JJL
54-
self.figure=figure
55+
self.figure = figure
5556
self._figdpi = figure.get_dpi()
5657

5758
self._bbox_inches_restore = bbox_inches_restore
@@ -68,6 +69,7 @@ def __init__(self, figure, width, height, dpi, vector_renderer,
6869
draw_gouraud_triangles option_scale_image
6970
_text2path _get_text_path_transform height width
7071
""".split()
72+
7173
def _set_current_renderer(self, renderer):
7274
self._renderer = renderer
7375

@@ -90,7 +92,7 @@ def start_rasterizing(self):
9092
# change the dpi of the figure temporarily.
9193
self.figure.set_dpi(self.dpi)
9294

93-
if self._bbox_inches_restore: # when tight bbox is used
95+
if self._bbox_inches_restore: # when tight bbox is used
9496
r = process_figure_for_rasterizing(self.figure,
9597
self._bbox_inches_restore)
9698
self._bbox_inches_restore = r
@@ -114,12 +116,13 @@ def stop_rasterizing(self):
114116
if self._rasterizing == 0:
115117
self._set_current_renderer(self._vector_renderer)
116118

117-
width, height = self._width * self.dpi, self._height * self.dpi
119+
height = self._height * self.dpi
118120
buffer, bounds = self._raster_renderer.tostring_rgba_minimized()
119121
l, b, w, h = bounds
120122
if w > 0 and h > 0:
121-
image = frombuffer(buffer, w, h, True)
122-
image.is_grayscale = False
123+
image = np.frombuffer(buffer, dtype=np.uint8)
124+
image = image.reshape((h, w, 4))
125+
image = image[::-1]
123126
gc = self._renderer.new_gc()
124127
# TODO: If the mixedmode resolution differs from the figure's
125128
# dpi, the image must be scaled (dpi->_figdpi). Not all

0 commit comments

Comments
 (0)