Skip to content

Transparency always on for PNG in version 3.1.0 #14339

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
cLupus opened this issue May 27, 2019 · 12 comments · Fixed by #20221
Closed

Transparency always on for PNG in version 3.1.0 #14339

cLupus opened this issue May 27, 2019 · 12 comments · Fixed by #20221
Milestone

Comments

@cLupus
Copy link

cLupus commented May 27, 2019

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.

@cLupus cLupus changed the title Transparancy always on for PNG in version 3.1 Transparency always on for PNG in version 3.1.0 May 27, 2019
@QuLogic
Copy link
Member

QuLogic commented May 27, 2019

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

cc @anntzer

@QuLogic QuLogic added this to the v3.1.1 milestone May 27, 2019
@dstansby dstansby added the Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions. label May 27, 2019
@anntzer
Copy link
Contributor

anntzer commented May 27, 2019

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

@timhoffm
Copy link
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.

@cLupus
Copy link
Author

cLupus commented May 30, 2019

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?

@anntzer
Copy link
Contributor

anntzer commented May 30, 2019

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.

@tacaswell tacaswell removed the Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions. label Jun 10, 2019
@efiring
Copy link
Member

efiring commented Jun 17, 2019

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

@anntzer
Copy link
Contributor

anntzer commented Sep 29, 2019

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

@anntzer anntzer closed this as completed Sep 29, 2019
@michaelosthege
Copy link

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

@tacaswell
Copy link
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?

@michaelosthege
Copy link

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

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

@ToyKeeper
Copy link

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
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants