2

I am trying to convert a figure drawn using pyplot to an array, but I would like to eliminate any space outside of the plot before doing so. In my current approach, I am saving the figure to a temporary file (using the functionality of plt.savefig to eliminate any space outside the plot, i.e. using bbox_inches='tight' and pad_inches = 0), and then loading the image from the temporary file. Here's an MWE:

from PIL import Image
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot([0,1], color='black', linewidth=4)
plt.xlim([0,1])
plt.ylim([0,1])
ax.set_aspect('equal', adjustable='box')
plt.axis('off')
plt.savefig('./tmp.png', bbox_inches='tight', pad_inches = 0)
plt.close()
img_size = 128
img = Image.open('./tmp.png')
X = np.array(img)

This approach is undesirable, because of the time required to write the file and read it. I'm aware of the following method for going directly from the pixel buffer to an array:

from PIL import Image
import matplotlib.pyplot as plt
from matplotlib.backends.backend_agg import FigureCanvas
import numpy as np

fig, ax = plt.subplots()
canvas = FigureCanvas(fig)
ax.plot([0,1], color='black', linewidth=4)
plt.xlim([0,1])
plt.ylim([0,1])
ax.set_aspect('equal', adjustable='box')
plt.axis('off')
canvas.draw()
X = np.array(canvas.renderer.buffer_rgba())

However, with this approach, I'm not sure how to eliminate the space around the plot before converting to an array. Is there an equivalent to bbox_inches='tight' and pad_inches = 0 that doesn't involve using plt.savefig()?

1 Answer 1

2

Improved Answer

This seems to work for your case and should be fast. There may be better ways - I am happy to delete it if anyone knows something better:

#!/usr/bin/env python3

from PIL import Image
import matplotlib.pyplot as plt
from matplotlib.backends.backend_agg import FigureCanvas
import numpy as np

fig, ax = plt.subplots()
canvas = FigureCanvas(fig)
ax.plot([0,1], color='red', linewidth=4)
plt.xlim([0,1])
plt.ylim([0,1])
ax.set_aspect('equal', adjustable='box')
plt.axis('off')
canvas.draw()
X = np.array(canvas.renderer.buffer_rgba())

The code above is yours, the code below is mine:

# Get width and height of cnvas for reshaping
w, h = canvas.get_width_height()
Y = np.frombuffer(X,dtype=np.uint8).reshape((h,w,4))[...,0:3]

# Work out extent of image by inverting and looking for black - ASSUMES CANVAS IS WHITE
extent = np.nonzero(~Y)

top    = extent[0].min()
bottom = extent[0].max()
left   = extent[1].min()
right  = extent[1].max()

tight_img = Y[top:bottom,left:right,:]

# Save as image just to test - you don't want this bit    
Image.fromarray(tight_img).save('tight.png')

enter image description here

Original Answer

There may be a better way, but you could avoid writing to disk by writing to a memory-based BytesIO instead:

from io import BytesIO

buffer = BytesIO()
plt.savefig(buffer, format='png', bbox_inches='tight', pad_inches = 0)

Then do:

x = np.array(Image.open(buffer))

In fact, if you use:

plt.savefig(buffer, format='rgba', bbox_inches='tight', pad_inches = 0)

the buffer already has your array and you can avoid the PNG encoding/decoding as well as the disk I/O. The only issue is that, because it is raw, we don't know the dimensions of the image to reshape() the buffer. It is actually this on my machine but I got the dimensions by writing a PNG and checking its width and height:

arr = buffer.getvalue()
x = np.frombuffer(arr, dtype=np.uint8).reshape((398,412,4))

If someone comes up with something better, I'll delete this.

Sign up to request clarification or add additional context in comments.

4 Comments

I actually like the second approach more. The problem with the first approach is that (if I understand correctly) it only works if there's something drawn over the full extent of the plot, whereas I'd like to be able to select out the range corresponding to [0,1] along the X and Y axes, regardless of what's drawn within that range. My guess is that the PNG encoding/decoding takes far less time than writing/reading from disk? If so the approach using the buffer seems like it would work just fine.
However, I'm having difficulty getting from the buffer to an array. When I do X = np.array(Image.open(PNG)) I get the following error: *** ValueError: embedded null byte, would you mind sharing a MWE using that approach?
It's getting late here... will have a look over weekend and come back to you.
Sorry, I wasn't clear enough. I have updated my answer and show how to reload the image from the BytesIO buffer.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.