Plots shifted up or to the left a pixel or so

Hi all,

If you zoom in to the origin in the following figure:

fig = plt.figure()
ax = fig.add_subplot(1,1,1, aspect='equal')
ax.plot([-1,1],[-1,1], color='blue')
ax.set_xlim(-1.1,1.1)
ax.set_ylim(-1.1,1.1)
ax.spines['left'].set_position('zero')
ax.spines['right'].set_color('none')
ax.spines['bottom'].set_position('zero')
ax.spines['top'].set_color('none')
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')
fig.savefig('test.png',dpi=300)

you'll see that the blue line isn't quite centered at the origin (at least the origin marked by the spines). It appears to hit just above or just to the left of the origin. Notice that there is a bit of blue coloring in the second quadrant, but none in the fourth. Notice also that there are more pixels colored just below the x-axis in quadrant 3 than just above the axis in quadrant 1.

Several of us Sage developers have been practically pulling out our hair trying to trace down why our plots seem to be shifted up or to the left by a pixel or so. I think the above example illustrates what is going on.

Any ideas as to what is going on? I'm not sure if the problem is with the line or with the spines. The lines ax.plot([-1,1],[0,0]) and ax.plot([0,0],[-1,1]) seem to be right on center with the spines.

Thanks,

Jason

···

--
Jason Grout

The problem is the default quantizing of all rectilinear axis-aligned lines (which includes the spines). They are "rounded" to the nearest center pixels in order to make them less fuzzy.

However, there's actually a bug in the quantizer that your example illustrates. Since the spine lines in your example have a stroke width of 4 pixels, they should actually be rounded to the nearest pixel edge, not nearest center pixel. So the quantizing is causing this slight alignment problem *and* making the straight lines look fuzzier than they should. I'm planning on writing a patch that will take stroke width into account to address this. By coincidence only, this will also make your example plot look more accurate (but that's dependent on the specific scale being used).

The quantizing (once corrected) is a tradeoff to increase the sharpness of rectilinear lines (which is important to many, including myself) at the expense of some subpixel accuracy. The easy solution is to provide a global flag (similar to path.simplify) that would turn off quantization globally for those that want to live on the other side of the tradeoff. The harder solution would be to quantize the axes (or spines) and adjust the data accordingly based on the amount of shift. I'm really not sure how to make the latter work without completely reworking how quantization is currently implemented (the quantization is pretty ignorant of anything "global" other than what it is currently drawing).

Mike

···

On 06/11/2010 02:02 AM, Jason Grout wrote:

Hi all,

If you zoom in to the origin in the following figure:

fig = plt.figure()
ax = fig.add_subplot(1,1,1, aspect='equal')
ax.plot([-1,1],[-1,1], color='blue')
ax.set_xlim(-1.1,1.1)
ax.set_ylim(-1.1,1.1)
ax.spines['left'].set_position('zero')
ax.spines['right'].set_color('none')
ax.spines['bottom'].set_position('zero')
ax.spines['top'].set_color('none')
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')
fig.savefig('test.png',dpi=300)

you'll see that the blue line isn't quite centered at the origin (at
least the origin marked by the spines). It appears to hit just above or
just to the left of the origin. Notice that there is a bit of blue
coloring in the second quadrant, but none in the fourth. Notice also
that there are more pixels colored just below the x-axis in quadrant 3
than just above the axis in quadrant 1.

Several of us Sage developers have been practically pulling out our hair
trying to trace down why our plots seem to be shifted up or to the left
by a pixel or so. I think the above example illustrates what is going on.

Any ideas as to what is going on? I'm not sure if the problem is with
the line or with the spines. The lines ax.plot([-1,1],[0,0]) and
ax.plot([0,0],[-1,1]) seem to be right on center with the spines.

Thanks,

Jason

--
Jason Grout

------------------------------------------------------------------------------
ThinkGeek and WIRED's GeekDad team up for the Ultimate
GeekDad Father's Day Giveaway. ONE MASSIVE PRIZE to the
lucky parental unit. See the prize list and enter to win:
http://p.sf.net/sfu/thinkgeek-promo
_______________________________________________
Matplotlib-devel mailing list
Matplotlib-devel@lists.sourceforge.net
matplotlib-devel List Signup and Options
   
--
Michael Droettboom
Science Software Branch
Space Telescope Science Institute
Baltimore, Maryland, USA

This specific bug is fixed in r8414.

Mike

···

On 06/11/2010 09:46 AM, Michael Droettboom wrote:

However, there's actually a bug in the quantizer that your example
illustrates. Since the spine lines in your example have a stroke width
of 4 pixels, they should actually be rounded to the nearest pixel edge,
not nearest center pixel. So the quantizing is causing this slight
alignment problem *and* making the straight lines look fuzzier than they
should. I'm planning on writing a patch that will take stroke width
into account to address this. By coincidence only, this will also make
your example plot look more accurate (but that's dependent on the
specific scale being used).
   

--
Michael Droettboom
Science Software Branch
Space Telescope Science Institute
Baltimore, Maryland, USA

THANK YOU!!!

And thanks for the nice explanation. Karl-Dieter Crisman and I figured there was some sort of pixel rounding issue somewhere, but it was above our heads to try to find it.

Thanks,

Jason

···

On 6/11/10 9:44 AM, Michael Droettboom wrote:

On 06/11/2010 09:46 AM, Michael Droettboom wrote:
   

However, there's actually a bug in the quantizer that your example
illustrates. Since the spine lines in your example have a stroke width
of 4 pixels, they should actually be rounded to the nearest pixel edge,
not nearest center pixel. So the quantizing is causing this slight
alignment problem *and* making the straight lines look fuzzier than they
should. I'm planning on writing a patch that will take stroke width
into account to address this. By coincidence only, this will also make
your example plot look more accurate (but that's dependent on the
specific scale being used).

This specific bug is fixed in r8414.

I tested your fix, and now this example gives the same sort of problem (note that in this case, the spine is 1 pixel wide; I've just changed the dpi):

from matplotlib import pyplot as plt
import numpy as np
fig = plt.figure()

ax = fig.add_subplot(1,1,1, aspect='equal')
ax.plot([-1,1],[-1,1], color='blue')
ax.set_xlim(-1.1,1.1)
ax.set_ylim(-1.1,1.1)
ax.spines['left'].set_position('zero')
ax.spines['right'].set_color('none')
ax.spines['bottom'].set_position('zero')
ax.spines['top'].set_color('none')
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')
fig.savefig('test.png',dpi=100)

It appears that the true origin is at the top left corner of the 1-pixel intersection of the two spines in the above example.

Thanks again for your work on this!

Jason

···

On 6/11/10 9:44 AM, Michael Droettboom wrote:

On 06/11/2010 09:46 AM, Michael Droettboom wrote:
   

However, there's actually a bug in the quantizer that your example
illustrates. Since the spine lines in your example have a stroke width
of 4 pixels, they should actually be rounded to the nearest pixel edge,
not nearest center pixel. So the quantizing is causing this slight
alignment problem *and* making the straight lines look fuzzier than they
should. I'm planning on writing a patch that will take stroke width
into account to address this. By coincidence only, this will also make
your example plot look more accurate (but that's dependent on the
specific scale being used).

This specific bug is fixed in r8414.

Mike

That's exactly what I meant when I said "By coincidence only, this will also make your example plot look more accurate (but that's dependent on the specific scale being used)." This isn't a proper fix, other than to make even-width lines looking less fuzzy. The correct fix (to either make quanitizing an rcParam or to adjust the data based on it) is much more work.

Mike

···

On 06/11/2010 01:31 PM, jason-sage@...691... wrote:

On 6/11/10 9:44 AM, Michael Droettboom wrote:
   

On 06/11/2010 09:46 AM, Michael Droettboom wrote:

However, there's actually a bug in the quantizer that your example
illustrates. Since the spine lines in your example have a stroke width
of 4 pixels, they should actually be rounded to the nearest pixel edge,
not nearest center pixel. So the quantizing is causing this slight
alignment problem *and* making the straight lines look fuzzier than they
should. I'm planning on writing a patch that will take stroke width
into account to address this. By coincidence only, this will also make
your example plot look more accurate (but that's dependent on the
specific scale being used).

This specific bug is fixed in r8414.

Mike

I tested your fix, and now this example gives the same sort of problem
(note that in this case, the spine is 1 pixel wide; I've just changed
the dpi):
   

--
Michael Droettboom
Science Software Branch
Space Telescope Science Institute
Baltimore, Maryland, USA

However, there's actually a bug in the quantizer that your example
illustrates. Since the spine lines in your example have a stroke width
of 4 pixels, they should actually be rounded to the nearest pixel edge,
not nearest center pixel. So the quantizing is causing this slight
alignment problem *and* making the straight lines look fuzzier than they
should. I'm planning on writing a patch that will take stroke width
into account to address this. By coincidence only, this will also make
your example plot look more accurate (but that's dependent on the
specific scale being used).

This specific bug is fixed in r8414.

Mike

I tested your fix, and now this example gives the same sort of problem
(note that in this case, the spine is 1 pixel wide; I've just changed
the dpi):

That's exactly what I meant when I said "By coincidence only, this will
also make your example plot look more accurate (but that's dependent on
the specific scale being used)." This isn't a proper fix, other than to
make even-width lines looking less fuzzy. The correct fix (to either
make quanitizing an rcParam or to adjust the data based on it) is much
more work.

I don't see how any reasonable algorithm could do such a data adjustment in general, so if the half-pixel inaccuracy is a problem, then I think using an rcParam to turn off quantizing is the way to go.

It appears that the difficulty is that quantization is exposed at the python level only for collections, via iter_segments.

Eric

···

On 06/11/2010 07:39 AM, Michael Droettboom wrote:

On 06/11/2010 01:31 PM, jason-sage@...691... wrote:

On 6/11/10 9:44 AM, Michael Droettboom wrote:

On 06/11/2010 09:46 AM, Michael Droettboom wrote:

Mike

   

However, there's actually a bug in the quantizer that your example
illustrates. Since the spine lines in your example have a stroke width
of 4 pixels, they should actually be rounded to the nearest pixel edge,
not nearest center pixel. So the quantizing is causing this slight
alignment problem *and* making the straight lines look fuzzier than they
should. I'm planning on writing a patch that will take stroke width
into account to address this. By coincidence only, this will also make
your example plot look more accurate (but that's dependent on the
specific scale being used).

This specific bug is fixed in r8414.

Mike

I tested your fix, and now this example gives the same sort of problem
(note that in this case, the spine is 1 pixel wide; I've just changed
the dpi):

That's exactly what I meant when I said "By coincidence only, this will
also make your example plot look more accurate (but that's dependent on
the specific scale being used)." This isn't a proper fix, other than to
make even-width lines looking less fuzzy. The correct fix (to either
make quanitizing an rcParam or to adjust the data based on it) is much
more work.
     

I don't see how any reasonable algorithm could do such a data adjustment
in general, so if the half-pixel inaccuracy is a problem, then I think
using an rcParam to turn off quantizing is the way to go.
   

Yeah -- as I considered it further, I'm coming to the same conclusion. We could optimize for a single point, eg. (0, 0), but not for all ticks, gridlines etc.

It appears that the difficulty is that quantization is exposed at the
python level only for collections, via iter_segments.
   

Sort of. Lines (but none of the other artists) follow what is set by "set_snap" (the use of two terms for the same thing is also a problem, of course). This needs to be extended to other artists (and other relevant backend methods other than draw_path, if necessary). But I think for convenience, it should also be a global rcParam.

Mike

···

On 06/11/2010 01:54 PM, Eric Firing wrote:

On 06/11/2010 07:39 AM, Michael Droettboom wrote:

On 06/11/2010 01:31 PM, jason-sage@...691... wrote:

On 6/11/10 9:44 AM, Michael Droettboom wrote:

On 06/11/2010 09:46 AM, Michael Droettboom wrote:

Eric

Mike

------------------------------------------------------------------------------
ThinkGeek and WIRED's GeekDad team up for the Ultimate
GeekDad Father's Day Giveaway. ONE MASSIVE PRIZE to the
lucky parental unit. See the prize list and enter to win:
http://p.sf.net/sfu/thinkgeek-promo
_______________________________________________
Matplotlib-devel mailing list
Matplotlib-devel@lists.sourceforge.net
matplotlib-devel List Signup and Options
   
--
Michael Droettboom
Science Software Branch
Space Telescope Science Institute
Baltimore, Maryland, USA

I think a work-around, then (at least it seems to work for me), is setting both snap to False and antialiased to False for the spines. That won't solve the issue for other horizontal lines, but at least it takes care of having the correct origin for the intersection of the spines. (Correct me if I'm wrong, of course!)

from matplotlib import pyplot as plt
import numpy as np
fig = plt.figure()
ax = fig.add_subplot(1,1,1, aspect='equal')
line1=ax.plot([-1,1],[0,0], color='blue')
line2=ax.plot([-1,1],[-1,1], color='red',zorder=5)
ax.set_xlim(-1.1,1.1)
ax.set_ylim(-1.1,1.1)
ax.spines['left'].set_position('zero')
ax.spines['left'].set_snap(False)
ax.spines['left'].set_antialiased(False)
ax.spines['right'].set_color('none')
ax.spines['bottom'].set_position('zero')
ax.spines['bottom'].set_snap(False)
ax.spines['bottom'].set_antialiased(False)
ax.spines['top'].set_color('none')
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')
fig.savefig('test.png',dpi=100)

Thanks,

Jason

···

On 6/11/10 1:02 PM, Michael Droettboom wrote:

It appears that the difficulty is that quantization is exposed at the
python level only for collections, via iter_segments.

Sort of. Lines (but none of the other artists) follow what is set by
"set_snap" (the use of two terms for the same thing is also a problem,
of course). This needs to be extended to other artists (and other
relevant backend methods other than draw_path, if necessary). But I
think for convenience, it should also be a global rcParam.

I've committed a patch that provides a global snap setting in r8415. Set the rcParam "path.snap" to False to turn off all snapping (though it should be equivalent to your "set_snap" calls above -- just possibly more convenient).

However, I think turning anti-aliasing off will give you the same problem at some scales, as anti-aliasing has basically the same effect as snapping: rounding to integral pixel values. Try an odd dpi such as "67" for example.

Mike

···

On 06/11/2010 02:38 PM, jason-sage@...691... wrote:

On 6/11/10 1:02 PM, Michael Droettboom wrote:
   

It appears that the difficulty is that quantization is exposed at the
python level only for collections, via iter_segments.

Sort of. Lines (but none of the other artists) follow what is set by
"set_snap" (the use of two terms for the same thing is also a problem,
of course). This needs to be extended to other artists (and other
relevant backend methods other than draw_path, if necessary). But I
think for convenience, it should also be a global rcParam.
     

I think a work-around, then (at least it seems to work for me), is
setting both snap to False and antialiased to False for the spines.
That won't solve the issue for other horizontal lines, but at least it
takes care of having the correct origin for the intersection of the
spines. (Correct me if I'm wrong, of course!)

from matplotlib import pyplot as plt
import numpy as np
fig = plt.figure()
ax = fig.add_subplot(1,1,1, aspect='equal')
line1=ax.plot([-1,1],[0,0], color='blue')
line2=ax.plot([-1,1],[-1,1], color='red',zorder=5)
ax.set_xlim(-1.1,1.1)
ax.set_ylim(-1.1,1.1)
ax.spines['left'].set_position('zero')
ax.spines['left'].set_snap(False)
ax.spines['left'].set_antialiased(False)
ax.spines['right'].set_color('none')
ax.spines['bottom'].set_position('zero')
ax.spines['bottom'].set_snap(False)
ax.spines['bottom'].set_antialiased(False)
ax.spines['top'].set_color('none')
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')
fig.savefig('test.png',dpi=100)
   

--
Michael Droettboom
Science Software Branch
Space Telescope Science Institute
Baltimore, Maryland, USA

It appears that the difficulty is that quantization is exposed at the
python level only for collections, via iter_segments.

Sort of. Lines (but none of the other artists) follow what is set by
"set_snap" (the use of two terms for the same thing is also a problem,
of course). This needs to be extended to other artists (and other
relevant backend methods other than draw_path, if necessary). But I
think for convenience, it should also be a global rcParam.

I think a work-around, then (at least it seems to work for me), is
setting both snap to False and antialiased to False for the spines.
That won't solve the issue for other horizontal lines, but at least it
takes care of having the correct origin for the intersection of the
spines. (Correct me if I'm wrong, of course!)

from matplotlib import pyplot as plt
import numpy as np
fig = plt.figure()
ax = fig.add_subplot(1,1,1, aspect='equal')
line1=ax.plot([-1,1],[0,0], color='blue')
line2=ax.plot([-1,1],[-1,1], color='red',zorder=5)
ax.set_xlim(-1.1,1.1)
ax.set_ylim(-1.1,1.1)
ax.spines['left'].set_position('zero')
ax.spines['left'].set_snap(False)
ax.spines['left'].set_antialiased(False)
ax.spines['right'].set_color('none')
ax.spines['bottom'].set_position('zero')
ax.spines['bottom'].set_snap(False)
ax.spines['bottom'].set_antialiased(False)
ax.spines['top'].set_color('none')
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')
fig.savefig('test.png',dpi=100)

I've committed a patch that provides a global snap setting in r8415.
Set the rcParam "path.snap" to False to turn off all snapping (though it
should be equivalent to your "set_snap" calls above -- just possibly
more convenient).

However, I think turning anti-aliasing off will give you the same
problem at some scales, as anti-aliasing has basically the same effect
as snapping: rounding to integral pixel values. Try an odd dpi such as
"67" for example.

I think you meant to say that aliasing is snapping, so with anti-aliasing off, aliasing is universal, and all points are snapped to pixels.

Eric

···

On 06/11/2010 09:09 AM, Michael Droettboom wrote:

On 06/11/2010 02:38 PM, jason-sage@...691... wrote:

On 6/11/10 1:02 PM, Michael Droettboom wrote:

Mike