savefig() renders paths and text differently than show() #786

Closed
karmel opened this Issue Mar 21, 2012 · 28 comments

Comments

Projects
None yet
9 participants
@karmel

karmel commented Mar 21, 2012

After customizing a number of parameters in my matplotlibrc file, I was able to generate figures as desired in with pyplot.show(). However, using pyplot.savefig() with the same parameters resulted in images with thicker lines and bigger fonts, even when the dpi was the same for the default figure and the saved image.

This issue was described by another user here: http://stackoverflow.com/questions/7906365/matplotlib-savefig-plots-different-from-show/9814313

I was able to find a solution for myself as described here: http://stackoverflow.com/questions/7906365/matplotlib-savefig-plots-different-from-show/9814313#9814313

But, it would be really nice if a fix were built in for all the rendering backends, or, alternately, there was at least an easy way to scale paths and fonts so that savefig() and show() resulted in similar images.

@mdboom

This comment has been minimized.

Show comment
Hide comment
@mdboom

mdboom Mar 21, 2012

Member

I can't reproduce the problem if the dpi's are the same -- I get identical images. Try this:

from matplotlib.pyplot import *
plot([1,2,3])
savefig("test.png", dpi=gcf().dpi)
show()

If you see differences, can you please post both the test.png and a screenshot of the window?

Which backend are you using? I suspect it's one of the non-Agg ones, or the changes you propose would have effect in both the screen and file output.

Member

mdboom commented Mar 21, 2012

I can't reproduce the problem if the dpi's are the same -- I get identical images. Try this:

from matplotlib.pyplot import *
plot([1,2,3])
savefig("test.png", dpi=gcf().dpi)
show()

If you see differences, can you please post both the test.png and a screenshot of the window?

Which backend are you using? I suspect it's one of the non-Agg ones, or the changes you propose would have effect in both the screen and file output.

@karmel

This comment has been minimized.

Show comment
Hide comment
@karmel

karmel Mar 22, 2012

Interesting. That does produce the same image for me. So, I was able to track down the source of the problem, which seems to be having figure.dpi is set to a non-default value in my matplotlibrc file.

So, if I create a matplotlibrc file from the one posted here: http://matplotlib.sourceforge.net/users/customizing.html

And at line 280 set figure.dpi = 160, then the resultant savefig() and show() outputs are different. You can see the difference here: http://i.imgur.com/Ap8NX.png , with the saved image on top of the show() image.

That's promising, because switching that setting back is likely an easier fix than the one I implemented. Is that desired behavior, though? Why would that happen?

Thanks!

karmel commented Mar 22, 2012

Interesting. That does produce the same image for me. So, I was able to track down the source of the problem, which seems to be having figure.dpi is set to a non-default value in my matplotlibrc file.

So, if I create a matplotlibrc file from the one posted here: http://matplotlib.sourceforge.net/users/customizing.html

And at line 280 set figure.dpi = 160, then the resultant savefig() and show() outputs are different. You can see the difference here: http://i.imgur.com/Ap8NX.png , with the saved image on top of the show() image.

That's promising, because switching that setting back is likely an easier fix than the one I implemented. Is that desired behavior, though? Why would that happen?

Thanks!

@pelson

This comment has been minimized.

Show comment
Hide comment
@pelson

pelson Sep 2, 2012

Member

To either @karmel or @mdboom : I think it would be helpful at this point to clarify what each of you thinks the action for this issue now is.

Thanks,

Member

pelson commented Sep 2, 2012

To either @karmel or @mdboom : I think it would be helpful at this point to clarify what each of you thinks the action for this issue now is.

Thanks,

@efiring

This comment has been minimized.

Show comment
Hide comment
@efiring

efiring Sep 3, 2012

Member

@karmel is pointing out a genuine bug that I can reproduce with the macosx backend, but not with *agg backends--which is why saving to a png works.

import matplotlib
matplotlib.use("macosx")
matplotlib.rcParams['figure.dpi'] = 240
import matplotlib.pyplot as plt

fig, ax = plt.subplots(1, figsize=(3,2)) # 2 inches high
ax.plot([1,2,3], lw=72) # line 1 inch wide
fig.savefig("testdpi.png", dpi=240)
plt.show()

This should draw a diagonal line of width half the height of the whole figure. With agg it does; with macosx it does not.

Member

efiring commented Sep 3, 2012

@karmel is pointing out a genuine bug that I can reproduce with the macosx backend, but not with *agg backends--which is why saving to a png works.

import matplotlib
matplotlib.use("macosx")
matplotlib.rcParams['figure.dpi'] = 240
import matplotlib.pyplot as plt

fig, ax = plt.subplots(1, figsize=(3,2)) # 2 inches high
ax.plot([1,2,3], lw=72) # line 1 inch wide
fig.savefig("testdpi.png", dpi=240)
plt.show()

This should draw a diagonal line of width half the height of the whole figure. With agg it does; with macosx it does not.

@mdboom

This comment has been minimized.

Show comment
Hide comment
@mdboom

mdboom Sep 4, 2012

Member

Can anyone on a mac look into why this is happening?

Member

mdboom commented Sep 4, 2012

Can anyone on a mac look into why this is happening?

@WeatherGod

This comment has been minimized.

Show comment
Hide comment
@WeatherGod

WeatherGod Sep 4, 2012

Member

I wonder if this is related to another bug I encountered with a friend who uses a Mac. In that case, the graphics context was handled differently in the macosx and the agg backends. Essentially, the color of the hatches were being handled properly in one, but not the other (I forget which).

Member

WeatherGod commented Sep 4, 2012

I wonder if this is related to another bug I encountered with a friend who uses a Mac. In that case, the graphics context was handled differently in the macosx and the agg backends. Essentially, the color of the hatches were being handled properly in one, but not the other (I forget which).

@dmcdougall

This comment has been minimized.

Show comment
Hide comment
@dmcdougall

dmcdougall Sep 4, 2012

Member

Out of interest. Are you sure lw=72 is correct, and not 72.27? I've seen the number 72.27 appearing in the PGF backend...

Member

dmcdougall commented Sep 4, 2012

Out of interest. Are you sure lw=72 is correct, and not 72.27? I've seen the number 72.27 appearing in the PGF backend...

@dmcdougall

This comment has been minimized.

Show comment
Hide comment
@dmcdougall

dmcdougall Sep 4, 2012

Member

Hmm, so it appears that there are 72 postscript points to an inch, but 72.27 LaTeX points to an inch. I'm on a Mac, I can take a quick look at this.

Member

dmcdougall commented Sep 4, 2012

Hmm, so it appears that there are 72 postscript points to an inch, but 72.27 LaTeX points to an inch. I'm on a Mac, I can take a quick look at this.

@dmcdougall

This comment has been minimized.

Show comment
Hide comment
@dmcdougall

dmcdougall Sep 4, 2012

Member

So, here's the interesting thing. When using savefig, the resulting figure's line is rather large, much bigger than one inch. However, using show, the line appears to be about an inch thick, which is what it should be. So I conclude that the problem is not with show but with savefig. Unless I'm missing something?

Member

dmcdougall commented Sep 4, 2012

So, here's the interesting thing. When using savefig, the resulting figure's line is rather large, much bigger than one inch. However, using show, the line appears to be about an inch thick, which is what it should be. So I conclude that the problem is not with show but with savefig. Unless I'm missing something?

@dmcdougall

This comment has been minimized.

Show comment
Hide comment
@dmcdougall

dmcdougall Sep 4, 2012

Member

There also appears to be lots of variables called dpi. There's figure.dpi, figure.canvas.dpi, renderer.dpi and when pushing the save button on the interactive toolbar, a 100 dpi figure is saved as a result of some dpi swapping. I am utterly confused as to why there isn't one single dpi value.

Member

dmcdougall commented Sep 4, 2012

There also appears to be lots of variables called dpi. There's figure.dpi, figure.canvas.dpi, renderer.dpi and when pushing the save button on the interactive toolbar, a 100 dpi figure is saved as a result of some dpi swapping. I am utterly confused as to why there isn't one single dpi value.

@efiring

This comment has been minimized.

Show comment
Hide comment
@efiring

efiring Sep 4, 2012

Member

@dmcdougall: Careful: which backend are you using? And are you measuring the thickness of the line (perpendicular, of course; I should have just made the line horizontal) as a fraction of the height of the figure? It should be 1/2 the height of the figure, since I specified the figure as 2 inches. When I use an agg backend, this is what I see; when I use macosx, it is not, and it is the screen line that is thinner than 1/2 the figure height.

Regarding dpi: yes, it is a confusing mess, and there is more than one way in which dpi operates, depending on backend.

Member

efiring commented Sep 4, 2012

@dmcdougall: Careful: which backend are you using? And are you measuring the thickness of the line (perpendicular, of course; I should have just made the line horizontal) as a fraction of the height of the figure? It should be 1/2 the height of the figure, since I specified the figure as 2 inches. When I use an agg backend, this is what I see; when I use macosx, it is not, and it is the screen line that is thinner than 1/2 the figure height.

Regarding dpi: yes, it is a confusing mess, and there is more than one way in which dpi operates, depending on backend.

@dmcdougall

This comment has been minimized.

Show comment
Hide comment
@dmcdougall

dmcdougall Sep 5, 2012

Member

@efiring I copied and pasted your code, so I'm using the macosx backend. I was measuring the diagonal thickness of the line, since that's the width of the line. Your comment leads me to believe that the diagonal thickness of the line should actually be two inches. Is that correct? I'll check against Agg.

Member

dmcdougall commented Sep 5, 2012

@efiring I copied and pasted your code, so I'm using the macosx backend. I was measuring the diagonal thickness of the line, since that's the width of the line. Your comment leads me to believe that the diagonal thickness of the line should actually be two inches. Is that correct? I'll check against Agg.

@dmcdougall

This comment has been minimized.

Show comment
Hide comment
@dmcdougall

dmcdougall Sep 5, 2012

Member

@efiring Using savefig, I get the same figure using either the agg or macosx backends. Unsurprising, since the macosx backend switches the backend to agg to save the figure anyway. Here is the output using savefig from the agg backend:

agg

And here is the output using savefig from the macosx backend:

osx

I even ran diff between them. The diff is empty.

Member

dmcdougall commented Sep 5, 2012

@efiring Using savefig, I get the same figure using either the agg or macosx backends. Unsurprising, since the macosx backend switches the backend to agg to save the figure anyway. Here is the output using savefig from the agg backend:

agg

And here is the output using savefig from the macosx backend:

osx

I even ran diff between them. The diff is empty.

@efiring

This comment has been minimized.

Show comment
Hide comment
@efiring

efiring Sep 5, 2012

Member

@dmcdougall, the confusion here is that one must compare what the macosx backend puts on the screen to what is written to the file. It is not using agg when rendering to the screen, but yes, it is using agg when it writes a png, regardless of whether it is done via savefig or via the file save button. The images you show here are correct (line thickness is half the figure height), but I think that if you estimate the line thickness as shown on your screen you will find it is considerably thinner than half the screen height.

Member

efiring commented Sep 5, 2012

@dmcdougall, the confusion here is that one must compare what the macosx backend puts on the screen to what is written to the file. It is not using agg when rendering to the screen, but yes, it is using agg when it writes a png, regardless of whether it is done via savefig or via the file save button. The images you show here are correct (line thickness is half the figure height), but I think that if you estimate the line thickness as shown on your screen you will find it is considerably thinner than half the screen height.

@dmcdougall

This comment has been minimized.

Show comment
Hide comment
@dmcdougall

dmcdougall Sep 5, 2012

Member

@efiring Ah, I understand now. Thanks for the explanation.

Member

dmcdougall commented Sep 5, 2012

@efiring Ah, I understand now. Thanks for the explanation.

@efiring

This comment has been minimized.

Show comment
Hide comment
@efiring

efiring Sep 5, 2012

Member

src/_macosx.m
That's the objective-C part where the real work is done.

Member

efiring commented Sep 5, 2012

src/_macosx.m
That's the objective-C part where the real work is done.

@dmcdougall

This comment has been minimized.

Show comment
Hide comment
@dmcdougall

dmcdougall Sep 5, 2012

Member

I'm making progress on this.

@efiring Thanks. I edited the comment once I found it :)

Member

dmcdougall commented Sep 5, 2012

I'm making progress on this.

@efiring Thanks. I edited the comment once I found it :)

@dmcdougall

This comment has been minimized.

Show comment
Hide comment
@dmcdougall

dmcdougall Sep 5, 2012

Member

I've found the problem. The OS X backend just ignores the linewidth parameter; it's never passed to the Core Graphics context.

Here's a screenshot for proof:

osx

I can now sleep! The next step is to fix it, which I need to figure out. But for now it's bedtime. Thanks for the help @efiring!

Member

dmcdougall commented Sep 5, 2012

I've found the problem. The OS X backend just ignores the linewidth parameter; it's never passed to the Core Graphics context.

Here's a screenshot for proof:

osx

I can now sleep! The next step is to fix it, which I need to figure out. But for now it's bedtime. Thanks for the help @efiring!

@mdboom

This comment has been minimized.

Show comment
Hide comment
@mdboom

mdboom Sep 6, 2012

Member

Thanks for everyone's hard work on this!

Member

mdboom commented Sep 6, 2012

Thanks for everyone's hard work on this!

@efiring

This comment has been minimized.

Show comment
Hide comment
@efiring

efiring Sep 6, 2012

Member

@dmcdougall, that screenshot above: is that what you get with the unmodified macosx backend? It is not the same as what I am seeing; the diagonal line in your version is the correct thickness, the same as in the agg images, whereas I see a line about 1/3 as thick, as a fraction of the figure height. What is the same in your screenshot and on my screen is that the ticks and the surrounding box are rendered differently than in the agg output.

A little more experimentation indicates that the macosx on-screen linewidth is always based on some fixed dpi; it is simply not seeing the figure.dpi setting. For example, if I use rcParams["figure.dpi"]=72, I get a small plot with everything scaled down except the width of the blue line, and the widths of the axis box and ticks.

Member

efiring commented Sep 6, 2012

@dmcdougall, that screenshot above: is that what you get with the unmodified macosx backend? It is not the same as what I am seeing; the diagonal line in your version is the correct thickness, the same as in the agg images, whereas I see a line about 1/3 as thick, as a fraction of the figure height. What is the same in your screenshot and on my screen is that the ticks and the surrounding box are rendered differently than in the agg output.

A little more experimentation indicates that the macosx on-screen linewidth is always based on some fixed dpi; it is simply not seeing the figure.dpi setting. For example, if I use rcParams["figure.dpi"]=72, I get a small plot with everything scaled down except the width of the blue line, and the widths of the axis box and ticks.

@dmcdougall

This comment has been minimized.

Show comment
Hide comment
@dmcdougall

dmcdougall Sep 6, 2012

Member

@efiring No, it's what I get after fixing the problem.

Member

dmcdougall commented Sep 6, 2012

@efiring No, it's what I get after fixing the problem.

@mdehoon

This comment has been minimized.

Show comment
Hide comment
@mdehoon

mdehoon Sep 9, 2012

Contributor

This doesn't seem the right solution to this problem.
gc.get_linewidth returns the line width previously set by gc.set_linewidth.
But gc.set_linewidth already makes the call to CGContextSetLineWidth, so we should not be needing another call to CGContextSetLineWidth in GraphicsContext_draw_path.

I think the issue lies in the definition of gc.set_linewidth:
Is the line width passed to gc.set_linewidth the line width in pixels? Or the line width in points?
And the same question for gc.get_linewidth: Is the line width returned by gc.get_linewidth the line width in pixels or in points?
I guess it's easiest if get/set_linewidth use units of pixels, and do the conversion between points and pixels uniformly for different backends if possible.
But if gc.set_linewidth must use units of points, then the conversion between points and pixels has to be made within the set_linewidth function of GraphicsContextMac. Which is a bit annoying, since the points_to_pixels function is in the renderer and not in the graphics context.

Contributor

mdehoon commented Sep 9, 2012

This doesn't seem the right solution to this problem.
gc.get_linewidth returns the line width previously set by gc.set_linewidth.
But gc.set_linewidth already makes the call to CGContextSetLineWidth, so we should not be needing another call to CGContextSetLineWidth in GraphicsContext_draw_path.

I think the issue lies in the definition of gc.set_linewidth:
Is the line width passed to gc.set_linewidth the line width in pixels? Or the line width in points?
And the same question for gc.get_linewidth: Is the line width returned by gc.get_linewidth the line width in pixels or in points?
I guess it's easiest if get/set_linewidth use units of pixels, and do the conversion between points and pixels uniformly for different backends if possible.
But if gc.set_linewidth must use units of points, then the conversion between points and pixels has to be made within the set_linewidth function of GraphicsContextMac. Which is a bit annoying, since the points_to_pixels function is in the renderer and not in the graphics context.

@dmcdougall

This comment has been minimized.

Show comment
Hide comment
@dmcdougall

dmcdougall Sep 9, 2012

Member

@mdehoon Thanks for taking the time to look at the pull request. I appreciate your feedback. I agree with most of what you say. In fact, in light of your comments the solution is much simpler and cleaner, but requires passing the dpi to the graphics context.

For comparison, let's take the agg backend. The dpi is passed in through the constructor and all points are converted to pixels in the c code. The dpi (as far as I am aware) is not currently passed to the objective code for the osx backend. Python passes the linewidths as points to the agg backend, and the conversion is done in c. I think, for the sake of attempting to keep everything uniform we should do the same here.

Since there are also problems with the mac backend not saving bitmaps are the correct dpi (issue ), I think it's best to change the GraphicsContextMac constructor to mimic that of the agg backend. That is, passing the width, height and dpi all to the C code. We could nail two birds with one stone if we do this properly.

I'll look into it more today. Thanks again for your comments.

Edit: The other mac dpi issue I forgot to mention. It is #113.

Member

dmcdougall commented Sep 9, 2012

@mdehoon Thanks for taking the time to look at the pull request. I appreciate your feedback. I agree with most of what you say. In fact, in light of your comments the solution is much simpler and cleaner, but requires passing the dpi to the graphics context.

For comparison, let's take the agg backend. The dpi is passed in through the constructor and all points are converted to pixels in the c code. The dpi (as far as I am aware) is not currently passed to the objective code for the osx backend. Python passes the linewidths as points to the agg backend, and the conversion is done in c. I think, for the sake of attempting to keep everything uniform we should do the same here.

Since there are also problems with the mac backend not saving bitmaps are the correct dpi (issue ), I think it's best to change the GraphicsContextMac constructor to mimic that of the agg backend. That is, passing the width, height and dpi all to the C code. We could nail two birds with one stone if we do this properly.

I'll look into it more today. Thanks again for your comments.

Edit: The other mac dpi issue I forgot to mention. It is #113.

@dmcdougall

This comment has been minimized.

Show comment
Hide comment
@dmcdougall

dmcdougall Sep 9, 2012

Member

On further inspection of the dpi issue, it has transpired that this is not an issue with osx backend in paritcular, see #1223.

Member

dmcdougall commented Sep 9, 2012

On further inspection of the dpi issue, it has transpired that this is not an issue with osx backend in paritcular, see #1223.

@dmcdougall

This comment has been minimized.

Show comment
Hide comment
@dmcdougall

dmcdougall Sep 10, 2012

Member

I think this can be closed now. See #1209.

Member

dmcdougall commented Sep 10, 2012

I think this can be closed now. See #1209.

@gepcel

This comment has been minimized.

Show comment
Hide comment
@gepcel

gepcel Jan 15, 2016

Contributor

I was googling something, and found this closed issue from long time ago. But I've come across this issue multiple times. Can anyone take a look?

The code as following:

mpl.rcParams['font.sans-serif'] = 'SimHei'
d = [19, 14, 8, 21, 17, 10, 16, 11, 8]
cmap=plt.get_cmap('Set1', 6)

fig = plt.figure(dpi=400)
for i, x in enumerate(d):
    plt.bar(i, x, color=cmap(i%3))

ax = gca()
xlim(-0.2, 9)
_ = plt.xticks(range(9), ['合计', '鱼卵', '仔稚鱼']*3, rotation='horizontal')
w = width = ax.patches[0].get_width()

ax.xaxis.set_minor_locator(FixedLocator(np.arange(9)+width/2))
ax.xaxis.set_minor_formatter(FixedFormatter(['合计', '鱼卵', '仔稚鱼']*3))
ax.xaxis.set_major_locator(FixedLocator(np.array([1, 4, 7])+width/2))
ax.xaxis.set_major_formatter(FixedFormatter(['2012年6月', '2013年6月', '2014年6月']))
ax.xaxis.set_tick_params(which='major', pad=17)
ax.xaxis.set_tick_params(which='both', tick1On=False, tick2On=False)
ax.yaxis.set_tick_params(which='both', tick1On=False, tick2On=False, label1On=False)

for r in ax.patches:
    h = r.get_height()
    ax.text(r.get_x()+w/2, h+0.4, int(h), ha='center', va='center')
legend(ax.patches[:3], ['合计', '鱼卵', '仔稚鱼'], loc='upper right')
plt.tight_layout()
fig.savefig('a.png', dpi=fig.dpi)

Since I've turned '%matplotlib inline' on, so I got the fig as following(which I supposed is the result of show()):
image

And the 'a.png' from 'savefig()' is as following:
a

I'm doing some quick plot and formatting, using both 'pyplot' and 'pylab', sorry for the dirty code.
Some backgroud:

Windows 10;
matplotlib  '1.5.1rc1'
python 3.4
ipython 3.2.1
%matplotlib inline
%config InlineBackend.figure_format = 'svg'
Contributor

gepcel commented Jan 15, 2016

I was googling something, and found this closed issue from long time ago. But I've come across this issue multiple times. Can anyone take a look?

The code as following:

mpl.rcParams['font.sans-serif'] = 'SimHei'
d = [19, 14, 8, 21, 17, 10, 16, 11, 8]
cmap=plt.get_cmap('Set1', 6)

fig = plt.figure(dpi=400)
for i, x in enumerate(d):
    plt.bar(i, x, color=cmap(i%3))

ax = gca()
xlim(-0.2, 9)
_ = plt.xticks(range(9), ['合计', '鱼卵', '仔稚鱼']*3, rotation='horizontal')
w = width = ax.patches[0].get_width()

ax.xaxis.set_minor_locator(FixedLocator(np.arange(9)+width/2))
ax.xaxis.set_minor_formatter(FixedFormatter(['合计', '鱼卵', '仔稚鱼']*3))
ax.xaxis.set_major_locator(FixedLocator(np.array([1, 4, 7])+width/2))
ax.xaxis.set_major_formatter(FixedFormatter(['2012年6月', '2013年6月', '2014年6月']))
ax.xaxis.set_tick_params(which='major', pad=17)
ax.xaxis.set_tick_params(which='both', tick1On=False, tick2On=False)
ax.yaxis.set_tick_params(which='both', tick1On=False, tick2On=False, label1On=False)

for r in ax.patches:
    h = r.get_height()
    ax.text(r.get_x()+w/2, h+0.4, int(h), ha='center', va='center')
legend(ax.patches[:3], ['合计', '鱼卵', '仔稚鱼'], loc='upper right')
plt.tight_layout()
fig.savefig('a.png', dpi=fig.dpi)

Since I've turned '%matplotlib inline' on, so I got the fig as following(which I supposed is the result of show()):
image

And the 'a.png' from 'savefig()' is as following:
a

I'm doing some quick plot and formatting, using both 'pyplot' and 'pylab', sorry for the dirty code.
Some backgroud:

Windows 10;
matplotlib  '1.5.1rc1'
python 3.4
ipython 3.2.1
%matplotlib inline
%config InlineBackend.figure_format = 'svg'
@jenshnielsen

This comment has been minimized.

Show comment
Hide comment
@jenshnielsen

jenshnielsen Jan 15, 2016

Member

@gepcel I don't think your issue is related to this one. The inline backend actually calls savefig behind behind the scenes so what you are seeing is a difference between plt.savefig('a.png') and plt.savefig('a.svg') I am not sure why the Agg backend (which saves pngs) don't handle your fonts correctly but that seems to be what happens.

Can you open a new issue coping you post from here so that the issue isn't lost

Member

jenshnielsen commented Jan 15, 2016

@gepcel I don't think your issue is related to this one. The inline backend actually calls savefig behind behind the scenes so what you are seeing is a difference between plt.savefig('a.png') and plt.savefig('a.svg') I am not sure why the Agg backend (which saves pngs) don't handle your fonts correctly but that seems to be what happens.

Can you open a new issue coping you post from here so that the issue isn't lost

@gepcel

This comment has been minimized.

Show comment
Hide comment
@gepcel

gepcel Jan 15, 2016

Contributor

OK. Thanks. @jenshnielsen

Contributor

gepcel commented Jan 15, 2016

OK. Thanks. @jenshnielsen

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment