sdbbs
sdbbs

Reputation: 5384

Cannot change background color of matplotlib Figure (when saving as image)?

The actual problem that I have, is that - in the code I'm working on: when I try to save the plot image, I get the plot background in the image as fully transparent, so it looks like this in an image viewer (here eom, Mate's version of eog):

transparent background

I would like to have the background color to be fully opaque white, instead.

This seems to have been a problem often, e.g.:

Anyways, one problem is that I cannot reproduce this with a minimal example. In my code, essentially I have to convert the figure to an image first, then process that image, then finally save it. So I tried making a minimal example, with a conversion to image data first, which is then being saved:

import matplotlib as mpl
print(f"{mpl.__version__}")
import matplotlib.pyplot as plt
import numpy as np
import io

t = np.arange(0.0, 2.0, 0.01)
s = np.sin(2*np.pi*t)

subplotpars = dict(left = 0.05, right=0.99, top=0.89, wspace=0.1)
gss = mpl.gridspec.GridSpec(2,1, height_ratios=(2, 1), **subplotpars),

fig = plt.figure()
gs = gss[0]
ax = fig.add_subplot(gs[0,0])

ax.plot(t, s, color=(0.4, 0.2, 0.6, 0.6))

print(f"{fig.get_facecolor()=}, {fig.get_alpha()=}")
print(f"{fig.patch.get_facecolor()=}, {fig.patch.get_alpha()=}")

with io.BytesIO() as buf:
    fig.savefig(buf, dpi=120, format="png")
    buf.seek(0)
    fig_rgba = plt.imread(buf)

plt.imsave('example_io.png', fig_rgba, dpi=120)

... and this code prints this for me:

3.5.1
fig.get_facecolor()=(1.0, 1.0, 1.0, 1.0), fig.get_alpha()=None
fig.patch.get_facecolor()=(1.0, 1.0, 1.0, 1.0), fig.patch.get_alpha()=None

... and tells me that I have matplotlib 3.5.1, and that the figure background color is defined as RGBA, although with alpha channel set to 1 (opaque). Ultimately, the image that is output by this code, for me, is with fully opaque white background - so this code does not demonstrate the problem.

So, while I cannot reconstruct my problem as a minimal example - I managed to narrow down the problem in my actual code, in this snippet:

        # ... time to save as image:

        print(f"A {self.fig.get_facecolor()=}, {self.fig.get_alpha()=}")
        print(f"A {self.fig.patch.get_facecolor()=}, {self.fig.patch.get_alpha()=}")

        orig_figcol = self.fig.get_facecolor()
        orig_figpatchcol = self.fig.patch.get_facecolor()
        self.fig.set_facecolor( (1.0, 1.0, 1.0, 1.0) ) # calls patch.set_facecolor anyway
        self.fig.patch.set_facecolor( (1.0, 1.0, 1.0, 1.0) )
        self.fig.canvas.draw()

        print(f"B {self.fig.get_facecolor()=}, {self.fig.get_alpha()=}")
        print(f"B {self.fig.patch.get_facecolor()=}, {self.fig.patch.get_alpha()=}")

        with io.BytesIO() as buf:
        # ...

When this section in the code is ran, I get a printout:

A self.fig.get_facecolor()=(1.0, 1.0, 1.0, 0), self.fig.get_alpha()=None
A self.fig.patch.get_facecolor()=(1.0, 1.0, 1.0, 0), self.fig.patch.get_alpha()=0
B self.fig.get_facecolor()=(1.0, 1.0, 1.0, 0), self.fig.get_alpha()=None
B self.fig.patch.get_facecolor()=(1.0, 1.0, 1.0, 0), self.fig.patch.get_alpha()=0

So, unlike the basic example above, the starting background color of the plot, as RGBA, is (1.0, 1.0, 1.0, 0) - that is to say, alpha channel is zero (so no surprise the background is transparent).

However, I do call set_facecolor right after, with an RGBA color where alpha is 1, and I even call .draw(), to hopefully trigger a refresh/redraw -- but regardless, when try to get the same facecolor I just previously set, it still returns the old color value, where alpha was 0!

So, essentially, the way I see this is: something does not let me change the Figure background color, right before I want to save the figure as an image.

Under what circumstances would matplotlib not let me change the image background - and ultimately, how can I change the plot figure background color, right before I save the figure as image (or export it as image data, that is, pixels)?

Upvotes: 2

Views: 2593

Answers (1)

sdbbs
sdbbs

Reputation: 5384

Ok, found the solution - this required some debugging and drilling down of code; it turns out, one of the forest of libraries I include in my code, decided to change matplotlib figure properties - so here is a bit of a writeup:

So, after my attempt to use pdb to debug this failed (Inspecting a variable value in other stack frames in pdb?), I found How do you watch a variable in pdb - which recommended the watchpoints library.

So basically, after my figure init (self.fig = plt.figure(...), I did:

watch(self.fig.patch._facecolor)

... and ran the code; and finally I got something like this in the terminal printout:

====== Watchpoints Triggered ======
Call Stack (most recent call last):
...
>   figure.patch.set_alpha(0)
  set_alpha (~/venv/lib/python3.8/site-packages/matplotlib/patches.py:394):
>   self._set_facecolor(self._original_facecolor)
  to_rgba (~/venv/lib/python3.8/site-packages/matplotlib/colors.py:192):
>   return rgba
self.fig.patch._facecolor:
(1.0, 1.0, 1.0, 1.0)
->
(1.0, 1.0, 1.0, 0)

So, one of these libraries ended up calling figure.patch.set_alpha(0) - which turns out to be a bit nastier than one would deduce from the naming alone; namely, once it is set to 0 - it will prohibit entering any other alpha value, whenever you want to set the face color!!! Here is a code that reproduces that, based on the OP code:

import matplotlib as mpl
print(f"{mpl.__version__}")
import matplotlib.pyplot as plt
import numpy as np
import io

t = np.arange(0.0, 2.0, 0.01)
s = np.sin(2*np.pi*t)

subplotpars = dict(left = 0.05, right=0.99, top=0.89, wspace=0.1)
gss = mpl.gridspec.GridSpec(2,1, height_ratios=(2, 1), **subplotpars),

fig = plt.figure()
gs = gss[0]
ax = fig.add_subplot(gs[0,0])

ax.plot(t, s, color=(0.4, 0.2, 0.6, 0.6))

print(f"A {fig.get_facecolor()=}, {fig.get_alpha()=}")
print(f"A {fig.patch.get_facecolor()=}, {fig.patch.get_alpha()=}")

fig.patch.set_alpha(0)

print(f"B {fig.get_facecolor()=}, {fig.get_alpha()=}")
print(f"B {fig.patch.get_facecolor()=}, {fig.patch.get_alpha()=}")

fig.patch.set_facecolor( (0.9, 0.9, 0.9, 1.0) )

print(f"C {fig.get_facecolor()=}, {fig.get_alpha()=}")
print(f"C {fig.patch.get_facecolor()=}, {fig.patch.get_alpha()=}")

with io.BytesIO() as buf:
    fig.savefig(buf, dpi=120, format="png", facecolor=(1.0, 1.0, 1.0, 1.0))
    buf.seek(0)
    fig_rgba = plt.imread(buf)

plt.imsave('example_io.png', fig_rgba, dpi=120)

This prints:

3.5.1
A fig.get_facecolor()=(1.0, 1.0, 1.0, 1.0), fig.get_alpha()=None
A fig.patch.get_facecolor()=(1.0, 1.0, 1.0, 1.0), fig.patch.get_alpha()=None
B fig.get_facecolor()=(1.0, 1.0, 1.0, 0), fig.get_alpha()=None
B fig.patch.get_facecolor()=(1.0, 1.0, 1.0, 0), fig.patch.get_alpha()=0
C fig.get_facecolor()=(0.9, 0.9, 0.9, 0), fig.get_alpha()=None
C fig.patch.get_facecolor()=(0.9, 0.9, 0.9, 0), fig.patch.get_alpha()=0

So - with fig.patch.set_alpha(0) having ran once, even if I set fig.patch.set_facecolor( (0.9, 0.9, 0.9, 1.0) ) afterward (which explicitly sets alpha to 0) - the actual fill color that ends up in the object, still has alpha=0 (as printout C shows)!

So if you run that code, you still get transparent background in the image.

The solution turns out to be - just set fig.patch.set_alpha(1.0) right before the fig.savefig call:

# ...
with io.BytesIO() as buf:
    fig.patch.set_alpha(1.0)
    fig.savefig(buf, dpi=120, format="png", facecolor=(1.0, 1.0, 1.0, 1.0))
    # ...

Boy.... Now those were some wasted hours of my life... I wish someone would invent some machines, that you could program ... oh wait

Upvotes: 1

Related Questions