Skip to content

Matplotlib 3.2.1 savefig empty image when fig size matches data size exactly #17542

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
rdgraham opened this issue May 31, 2020 · 7 comments · Fixed by #17560
Closed

Matplotlib 3.2.1 savefig empty image when fig size matches data size exactly #17542

rdgraham opened this issue May 31, 2020 · 7 comments · Fixed by #17560
Labels
Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions.
Milestone

Comments

@rdgraham
Copy link

Bug report

Bug summary

Saving a figure to an image file with the same pixel dimensions as data loaded with imshow does not work in 3.2.1, producing an empty image. If the dimensions are changed by even one pixel, normal behavior returns.

This bug appeared in 3.2.1 and I have confirmed that it does not occur with 3.1.1.

I noticed in the release notes for 3.2.0 that some changes were made to the default image interpolation; perhaps an unhandled condition was introduced here when no interpolation is required. However, I note that changing the interpolation mode had no effect on this bug.

Code for reproduction

import numpy as np
from matplotlib import pylab as plt

x_size, y_size = (100, 100)
fig = plt.figure(frameon=False, dpi=100, figsize=(y_size/100, x_size/100))
ax = plt.Axes(fig, [0., 0., 1., 1.])
fig.add_axes(ax)
ax.set_axis_off()
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)

data = np.random.rand(100, 100)
ax.imshow(data)
fig.savefig('/tmp/image.png', bbox_inches='tight', pad_inches=0)

# In matplotlib 3.2.1, empty file with size 100x100 pixels output
# if random data shape is changed to something other than 100x100, output
# works correctly.

# In matplotlib 3.1.1, file output is 100x100 with data shown correctly.

Actual outcome
bug_image

Expected outcome

working_image

Produced with matplotlib 3.1.1

Matplotlib version

  • Operating system: Linux version 5.6.13-arch1-1
  • Matplotlib version: 3.2.1
  • Matplotlib backend (print(matplotlib.get_backend())): TkAgg
  • Python version: 3.8.3
  • Other libraries: numpy 1.18.4

Default system install on Arch linux package python-matplotlib-3.2.1-1-x86_64

@jklymak
Copy link
Member

jklymak commented May 31, 2020

If you remove pad_inches=0 this works fine.

This stopping working seems to bisect to a9b8b6e which I think has more to do with extents than the image work. #16734

@jklymak
Copy link
Member

jklymak commented May 31, 2020

Furthermore, if you do pad_inches=0.0000000001 it works fine, or the axes size to 0.999999 etc it works fine. So its definitely that you are hitting a precise integer somehow. But if you don't do the tight_bbox, or pad_inches, you get exactly what you want.

@rdgraham
Copy link
Author

Thanks for the quick response and work-around. Hopefully it will be possible to get to the underlying issue.

I don't recall the origin of the pad and bbox options in this code, though I would note that it is surprising that not specifying the pad_inches and bbox_inches options results in no padding in this case. From the documentation, the default value for pad_inches is 0.1 inch.

@jklymak
Copy link
Member

jklymak commented May 31, 2020

pad_inches only matters if you use bbox_inches='tight'

@tacaswell tacaswell added this to the v3.2.2 milestone Jun 2, 2020
@tacaswell tacaswell added the Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions. label Jun 2, 2020
@tacaswell
Copy link
Member

This was broken by #16734 which disables the draw methods on the renderer (because to compute the bounding boxes we don't need to actually draw turn the crank on the renderer methods, only the artist methods) but we never re-set the methods to their original values.

This is not normally a problem as the tight bounding box is a different size that original size so the caching in

def get_renderer(self, cleared=False):
w, h = self.figure.bbox.size
key = w, h, self.figure.dpi
reuse_renderer = (hasattr(self, "renderer")
and getattr(self, "_lastKey", None) == key)
if not reuse_renderer:
self.renderer = RendererAgg(w, h, self.figure.dpi)
self._lastKey = key
elif cleared:
self.renderer.clear()
return self.renderer
misses and we get a new, un-patched renderer. However, in this case the figure is already the correct size so we hit the cache and get back the renderer with the draw methods stripped off which then results in an empty output.

I can see a bunch of hacky ways to fix this, but not sure which one is the best.

@jklymak
Copy link
Member

jklymak commented Jun 3, 2020

It seems to me that draw_disabled=True should just make a new un-cached renderer?

@tacaswell
Copy link
Member

The problem is that where we know if we have flagged it as draw_disabled (in backend bases) is not where we know if there is a cache or not (deep in the print_XYZ functions in the backends).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants