55

I'm trying to get a numpy array image from a Matplotlib figure and I'm currently doing it by saving to a file, then reading the file back in, but I feel like there has to be a better way. Here's what I'm doing now:

from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
from matplotlib.figure import Figure

fig = Figure()
canvas = FigureCanvas(fig)
ax = fig.gca()

ax.text(0.0,0.0,"Test", fontsize=45)
ax.axis('off')

canvas.print_figure("output.png")
image = plt.imread("output.png")

I tried this:

image = np.fromstring( canvas.tostring_rgb(), dtype='uint8' )

from an example I found but it gives me an error saying that 'FigureCanvasAgg' object has no attribute 'renderer'.

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
John Stanford
  • 993
  • 1
  • 7
  • 18

5 Answers5

63

In order to get the figure contents as RGB pixel values, the matplotlib.backend_bases.Renderer needs to first draw the contents of the canvas.

You can do this by manually calling canvas.draw():

from matplotlib.figure import Figure

fig = Figure()
canvas = fig.canvas
ax = fig.gca()

ax.text(0.0,0.0,"Test", fontsize=45)
ax.axis('off')

canvas.draw()  # Draw the canvas, cache the renderer

image_flat = np.frombuffer(canvas.tostring_rgb(), dtype='uint8')  # (H * W * 3,)
# NOTE: reversed converts (W, H) from get_width_height to (H, W)
image = image_flat.reshape(*reversed(canvas.get_width_height()), 3)  # (H, W, 3)

See here for more info on the Matplotlib API.

Intrastellar Explorer
  • 3,005
  • 9
  • 52
  • 119
ali_m
  • 71,714
  • 23
  • 223
  • 298
  • 56
    I get the img as 1d array, you can fix that using: `width, height = fig.get_size_inches() * fig.get_dpi()` and `img = np.fromstring(canvas.to_string_rgb(), dtype='uint8').reshape(height, width, 3) ` – MaxNoe Mar 15 '17 at 08:51
  • 3
    I sometimes get an error where height and width are floats, converting them to integers is an easy fix though. – Jaden Travnik May 08 '18 at 14:21
  • 1
    I edited the answer to include @MaxNoe 's suggestion. – Waleed Abdulla May 14 '18 at 03:45
  • Do we really have to call `canvas.draw()` to make this work? – Rishabh Agrahari Mar 07 '19 at 15:23
  • 1
    @RishabhAgrahari One way or another the contents of the canvas need to have been rendered at least once before you can fetch the pixel values. Rendering can happen as a side-effect of other operations, e.g. if the canvas belongs to a pyplot figure and you call `plt.show()` to display it then the canvas will be rendered. However in the example above, if you get rid of the call to `canvas.draw` you'll get `AttributeError: 'FigureCanvasAgg' object has no attribute 'renderer'` (try it). – ali_m Mar 07 '19 at 21:26
29

For people who are searching an answer for this question, this is the code gathered from previous answers. Keep in mind that the method np.fromstring is deprecated and np.frombuffer is used instead.

#Image from plot
ax.axis('off')
fig.tight_layout(pad=0)

# To remove the huge white borders
ax.margins(0)

fig.canvas.draw()
image_from_plot = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8)
image_from_plot = image_from_plot.reshape(fig.canvas.get_width_height()[::-1] + (3,))
rayryeng
  • 102,964
  • 22
  • 184
  • 193
Jorge Diaz
  • 491
  • 5
  • 11
  • 1
    @rayryeng-ReinstateMonica Thank you for the changes that improves the answer significantly – Jorge Diaz Dec 03 '19 at 15:18
  • @rayreng Is it possible to get a grayscale output? I don't see a method similar to `tostring_rgb` on the canvas – Mehdi Zare Jul 14 '20 at 16:20
  • thanks for compiling answers! I should also add that the order of commands (especially `fig.canvas.draw()`) is very important. My code didn't work out initially due to wrong ordering. – Iman Mirzadeh Dec 29 '21 at 12:31
16

from the docs:

https://matplotlib.org/gallery/user_interfaces/canvasagg.html#sphx-glr-gallery-user-interfaces-canvasagg-py

fig = Figure(figsize=(5, 4), dpi=100)
# A canvas must be manually attached to the figure (pyplot would automatically
# do it).  This is done by instantiating the canvas with the figure as
# argument.
canvas = FigureCanvasAgg(fig)

# your plotting here

canvas.draw()
s, (width, height) = canvas.print_to_buffer()

# Option 2a: Convert to a NumPy array.
X = np.fromstring(s, np.uint8).reshape((height, width, 4))
MrE
  • 19,584
  • 12
  • 87
  • 105
10

I think there is some update, which is easier.

canvas.draw()
buf = canvas.buffer_rgba()
X = np.asarray(buf)

Updated version from the docs:

from matplotlib.backends.backend_agg import FigureCanvasAgg
from matplotlib.figure import Figure
import numpy as np

# make a Figure and attach it to a canvas.
fig = Figure(figsize=(5, 4), dpi=100)
canvas = FigureCanvasAgg(fig)

# Do some plotting here
ax = fig.add_subplot(111)
ax.plot([1, 2, 3])

# Retrieve a view on the renderer buffer
canvas.draw()
buf = canvas.buffer_rgba()
# convert to a NumPy array
X = np.asarray(buf)
starriet
  • 2,565
  • 22
  • 23
  • This is the version that worked for me. `np.fromstring` is deprecated, and without specifying `FigureCanvasAgg` some platforms give an error, on macOS for example`FigureCanvasMac` has no `renderer` attribute. I find incredible how complicated such an operation is :( – Jacopofar Oct 10 '20 at 14:55
  • To render an image of a specific size (for example, a 1024 by 512 image), do `fig = Figure(figsize=(1024, 512), dpi=1)` when constructing figure. – lingjiankong Sep 07 '21 at 19:42
  • 1
    @lingjiankong : no, because you'll get `RuntimeError: In set_size: Could not set the fontsize (error code 0x97)` because the `dpi` is set too low to render fonts. prefer a `fig = Figure(figsize=(10.24, 5.12), dpi=100.0)` instead, it won't change the final picture size, but it pleases `matplotlib` better this way. – Kochise Oct 28 '21 at 12:48
4

To fix the large margins Jorge references, add ax.margins(0). See here for details.

cdw
  • 41
  • 2