Skip to content

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

Closed

Description

@rdgraham

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

Activity

  1. jklymak commented on May 31, 2020

    @jklymak
    Member

    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

  2. jklymak commented on May 31, 2020

    @jklymak
    Member

    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.

  3. rdgraham commented on May 31, 2020

    @rdgraham
    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.

  4. jklymak commented on May 31, 2020

    @jklymak
    Member

    pad_inches only matters if you use bbox_inches='tight'

  5. added this to the v3.2.2 milestone on Jun 2, 2020
  6. added
    Release criticalFor bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions.
    on Jun 2, 2020
  7. tacaswell commented on Jun 3, 2020

    @tacaswell
    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.

  8. jklymak commented on Jun 3, 2020

    @jklymak
    Member

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

  9. tacaswell commented on Jun 3, 2020

    @tacaswell
    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).

  10. added 3 commits that reference this issue on Jun 3, 2020
    91e2fd8
    297cf79
    5324ada
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Release criticalFor bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions.

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions