Skip to content

Transparency always on for PNG in version 3.1.0 #14339

Closed

Description

@cLupus

Bug report

Bug summary

While saving a figure using savefig, the flag for transparency is ignored, and the resulting PNG has a transparent background, even though transparency is set to False.

Note: This affect matplotlib 3.1.0. The error did not occur in 3.0.3 on the same machine as described bellow.

Code for reproduction

# Paste your code here
from matplotlib import peplos as plt
fig = plt.figure(figsize=[20, 10], frameon=False)
fig.savefig('./test.png', transparent=False)
plt.close()

Actual outcome

An empty PNG file, that is transparent.

Expected outcome

An empty PNG file with only a white background.

Matplotlib version

  • Operating system: macOS 10.14.5 / CentOS 6.
  • Matplotlib version: 3.1.0
  • Matplotlib backend (print(matplotlib.get_backend())): module://backend_interagg
  • Python version: 3.6.1
  • Jupyter version (if applicable):
  • Other libraries:

Python was installed via pyenv (compiled from source), and matplotlib was installed through pipenv.

Activity

  1. changed the title [-]Transparancy always on for PNG in version 3.1[/-] [+]Transparency always on for PNG in version 3.1.0[/+] on May 27, 2019
  2. QuLogic commented on May 27, 2019

    @QuLogic
    Member

    This bisects to 38e3896, backport of #11692, and possibly the interaction between frameon and transparent was not realized at the time.

    cc @anntzer

  3. added this to the v3.1.1 milestone on May 27, 2019
  4. added
    Release criticalFor bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions.
    on May 27, 2019
  5. anntzer commented on May 27, 2019

    @anntzer
    Contributor

    I would say that this is actually closer to the indended behavior: you requested that the figure has no frame, and then the transparent kwarg is only documented to make the figure transparent if it is set to True (but not to undo transparency if it is set to False).

    Again, this points to the issue of having multiple kwargs that control the same thing: what happens when they are set in a conflicting manner?

    I guess, in the meantime, we could make transparent=False undo the transparency, if any (the default, transparent=None, would do nothing). Obviously this could also break some other users that were explicitly passing transparent=False and hoping that nothing happens, but at least they can pass transparent=None instead to achieve that...

  6. timhoffm commented on May 30, 2019

    @timhoffm
    Member

    transparent=None currently means use rcParams['savefig.transparent'] (default False). That will have an explicit effect on the axes patches. Meaning "do not change" for the figure patch at the same time would be even more confusing.

    There are two options:

    1. Make transparent always enables/disables visibility of the figure patch (None only meaning to use the rcParams).
    2. Let transparent only hide visible figure patches. This assumes that the user has intentionally hidden the figure patch and if he wants to draw it, he will explicitly enable it.

    2 is the current behavior and I'm leaning towards it. 1) doesn't make it possible to have axes patches but not figure patches visible.

  7. cLupus commented on May 30, 2019

    @cLupus
    Author

    The main issue, for me, is that the new behavior breaks a test in a project of mine (comparing a generated image with a reference). Is there an, preferably easy, or simple mitigation, or migration path?

  8. anntzer commented on May 30, 2019

    @anntzer
    Contributor

    The transparent kwarg is basically a red herring here, previously even

    from matplotlib import pyplot as plt
    fig = plt.figure(frameon=False)
    fig.subplots()
    fig.savefig('./test.png')
    plt.close()
    

    would result in a plot without a transparent background because background transparency would be read from rcParams["figure.frameon"].

    As argued elsewhere I think frameon is basically a bad idea...

    You can work around this (I think) with

    from matplotlib import pyplot as plt
    fig = plt.figure(facecolor="none")  # Sets the figure patch to transparent.
    fig.savefig('./test.png', facecolor="w")  # Sets the figure patch to solid white for saving.
    
  9. removed
    Release criticalFor bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions.
    on Jun 10, 2019
  10. efiring commented on Jun 17, 2019

    @efiring
    Member

    @tacaswell I think you were going to write an explanation and close this, with the idea that the present behavior is intended. Correct?

  11. anntzer commented on Sep 29, 2019

    @anntzer
    Contributor

    I'm going to claim that this is the intended behavior (see discussion above) and close this, especially as a workaround is available.

  12. michaelosthege commented on May 13, 2021

    @michaelosthege

    Would it be possible to print a warning when transparent=False is used?

    Also it looks like fig.canvas.tostring_rgb() is affected - at least it results in crappy quality:

    fig = pyplot.figure()
    pyplot.scatter([1,2,3], [1,3,2])
    
    # Convert the image to an RGB array
    fig.canvas.draw()
    pixels = numpy.fromstring(fig.canvas.tostring_rgb(), dtype="uint8")
    rgb = pixels.reshape(*fig.canvas.get_width_height()[::-1], 3)
    
    pyplot.close()
    
    pyplot.figure(dpi=100)
    pyplot.imshow(rgb)

    bug

  13. tacaswell commented on May 13, 2021

    @tacaswell
    Member

    The source of the problem with tostring_rgb is #5336 which is because it just truncates the alpha channel! Because of the way anti-aliasing is implemented in Agg, the edges of the letters are blended into what ever color is below it including the alpha. If you start with a (0, 0, 0, 0) as the background, the alpha survives all the way to the end, hence the RGB channels of the text are really chunky. If you grab the alpha channel and blend in into white "by hand" (or have a non-transparent figure patch), then it looks OK:

    import matplotlib.pyplot as pyplot
    import matplotlib as mpl
    from matplotlib.backends.backend_agg import FigureCanvas
    import numpy
    
    # make the figure "by hand" to avoid hi-dpi issues
    fig = mpl.figure.Figure(facecolor=(1, 1, 1, 0))
    cv = FigureCanvas(fig)
    fig.gca().scatter([1, 2, 3], [1, 3, 2])
    fig.canvas.draw()
    
    pixels = np.asarray(fig.canvas.buffer_rgba()) / 255
    
    
    pyplot.close()
    
    demo_fig, ax_d = pyplot.subplot_mosaic(
        [["r", "g", "b", "a"], ["rgb", "rgba", ".", "blend"]], constrained_layout=True
    )
    for k, v in ax_d.items():
        v.set_title(k)
    for indx, k in enumerate("rgba"):
        ax_d[k].imshow(pixels[:, :, indx])
    
    ax_d["rgb"].imshow(pixels[:, :, :3])
    ax_d["rgba"].imshow(pixels)
    ax_d["blend"].imshow(pixels[:, :, 0:3] * pixels[:, :, 3:] + 1 * (1 - pixels[:, :, 3:]))
    
    for k, v in ax_d.items():
        v.set_title(k)
        v.set_xlim(480, 600)
        v.set_ylim(480, 300)
    pyplot.show()

    so

    @michaelosthege It is also not clear to me in what set of conditions you would want a warning?

  14. michaelosthege commented on May 13, 2021

    @michaelosthege

    @tacaswell thanks for the explanation!

    I got bitten by this behavior pretty hard today, when I tried to make a GIF. My strategy of writing individual frames with savefig and then writing a GIF with imageio had worked just fine a few months back.
    But today the resulting GIFs were super ugly with the artifacts from above. For 2 hours I suspected compression when the GIF was created and tried PIL/imageio/moviepy - eventually figuring out that they all use PIL under the hood.
    All the frames I saved as intermediates looked fine!

    Only when I found my files from January and compared the intermediate frames (good thing I saved them to disk), I noticed that even though they were all PNG, the new ones were transparent.
    After finding this issue rather quickly, because transparent=False didn't work as advertised, it took me another hour to make it work with facecolor='w' and cutting away the alpha channel manually, because the PNGs were still saved with alpha.

    It is also not clear to me in what set of conditions you would want a warning?

    savefig(transparent=False) should not silently ignore the kwarg.

    Generally something like fig.to_rgba_array() and fig.to_rgb_array() would be great.

    All the solutions one finds when searching for "pyplot figure to numpy array" are really verbose code with buffers and reshaping. And they all suffer from #5336.

  15. tacaswell commented on May 13, 2021

    @tacaswell
    Member

    As of 3.1.0 you can do:

    pixels = np.asarray(fig.canvas.buffer_rgba()) / 255

    which gets you the full images as an array.

    The docs on this are:

            transparent : bool
                If *True*, the Axes patches will all be transparent; the
                figure patch will also be transparent unless facecolor
                and/or edgecolor are specified via kwargs.
                This is useful, for example, for displaying
                a plot on top of a colored background on a web page.  The
                transparency of these patches will be restored to their
                original values upon exit of this function.
    
    

    which says setting this to True will make the axes and figure patches transparent if it is True, it makes no claims as to what it will do you you set the value False. On one hand I see the motivation "False means the patch is forced to be not transparent", but "False means do nothing" is also reasonable (and is what it actually does!

    One key problem with trying to implement anything for False is that we have to pick some color to blend into (yes, white a a reasonable option, but fig.savefig already gives you a way to set the patch color!),

    The default value of transparent= is None in which case we fallback to the value in rcparams (which defaults to False which means "do nothing"). I guess we could warn when the user passes False explicitly to say "this probably is not doing what you think it is doing". However, the default default is also False, we do not want to warn on every savefig for a kwarg/rcParam that many users do not know exists so it seems a bit weird to warn in one case and not the other. Further, the default behavior of "please do not change my Figure and Axes patch color" seems like a reasonable thing for someone ask for defensively (in case they are a library author and do not want to risk their users rcparams messing with them).

    I'm going to open a PR adding "If False this will do nothing" to the docstring as I think that is the best course forward and will hopefully prevent future confusion / frustration.

  16. added a commit that references this issue on May 13, 2021
    6c2d146
  17. added a commit that references this issue on Jun 13, 2021
    bec5d72
  18. ToyKeeper commented on Feb 6, 2023

    @ToyKeeper

    Just another +1 to the visibility of this issue, because upgrading matplotlib broke all my graphs. I tried for several hours to fix it before I found this bug, and the solution wasn't any of the things I expected.

    To fix it, I added fig.set_frameon(True).

    Simple 1-line change, but definitely not obvious.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions