user1030718
user1030718

Reputation:

Sub-pixel accuracy scatter plots with matplotlib?

Note: I figured out a solution to this question while writing it. My answer is below.

Is there an easy way to get sub-pixel antialiased placement of circles using matplotlib? I was able to create the following .gif but the fact that the motion of the circles steps by integer pixels is really bugging me.

enter image description here

I can of course render a large image (plt.savefig("image.png",dpi=1000)) and scale down, but the added complexity is a pain (this is an example for students, so importing extra tools will annoy the students and installing extra tools will annoy campus IT!)

import matplotlib.pyplot as plt
from math import sin,cos
x=[]
y=[]
#Create 41**2 particles in the unit square
for i in range(41):
    for j in range(41):
        x.append(i*0.05-1.0)
        y.append(j*0.05-1.0)
#5 second video at 30 fps = 150 frames
for n in range(150):
    plt.close('all')
    fig, axes = plt.subplots(figsize=(5,5))
    #some cool motion
    for i in range(len(x)):
        x[i]+=0.001*cos(x[i]+3*y[i])
        y[i]+=0.001*sin(6*x[i]-4*y[i])
    #create the scatter plot
    axes.scatter(x,y,s=3,antialiased=True)
    axes.set_xlim(-1.4,1.4)
    axes.set_ylim(-1.4,1.4)
    axes.set_aspect('equal')
    plt.savefig("out/fig%03d.png"%n,dpi=80)

Upvotes: 3

Views: 1444

Answers (3)

antony
antony

Reputation: 2997

You may be interested in https://github.com/anntzer/mplcairo, a new cairo-based backend for Matplotlib which I originally wrote exactly because I was unhappy with this issue. It does not display the aforementioned problem, while maintaining reasonable performance.

Upvotes: 2

ImportanceOfBeingErnest
ImportanceOfBeingErnest

Reputation: 339630

The problem is based in the scatter marker stamping of the Agg backend. There is this issue about it, which is still unsolved.

However, using the Cairo backend, the issue is not present.

import matplotlib
matplotlib.use("TkCairo") # or Qt4Cairo, Qt5Cairo

However, there is an issue with saving the animation. This is fixed in this PR. Hence, an option is to replace your backend_cairo.py with the fixed version and use the Cairo backend.

The following would be an example which uses the animtion module, such that the animation can be observed live on screen as well as easily saved:

import numpy as np
import matplotlib
matplotlib.use("TkCairo")
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

x, y = np.meshgrid(np.linspace(-1,1,31),np.linspace(-1,1,31))

fig, ax = plt.subplots(figsize=(2.8,2.8))
fig.subplots_adjust(.02,.02,.98,.9)
ax.set_title("scatter")

### Scatter
sc = ax.scatter(x,y,s=2)

ax.axis("off")
ax.set_xlim(-1.2,1.2)
ax.set_ylim(-1.2,1.2)
ax.set_aspect('equal')

def generator():
    X=np.copy(x); Y=np.copy(y)
    for i in range(100):
        X += 0.0015*np.cos(x+3*y)
        Y += 0.0015*np.sin(6*x-4*y)
        yield (X,Y)

def animate(i): 
    x,y = i
    # update scatter
    sc.set_offsets(np.c_[x.flatten(),y.flatten()])

ani = FuncAnimation(fig, animate, frames=generator, interval=50, repeat=False)
ani.save("skewedgrid_scatter.gif", writer="imagemagick")
plt.show()

enter image description here

Upvotes: 1

user1030718
user1030718

Reputation:

The scatter function must be optimized to just do quick integer placement. If you manually add Circle objects, the sub-pixel antialiasing is handled for you. It's quite a bit slower. Example:

import matplotlib.pyplot as plt
from matplotlib.collections import PatchCollection
from math import sin,cos
x=[]
y=[]
#Create 41**2 particles in the unit square
for i in range(41):
    for j in range(41):
        x.append(i*0.05-1.0)
        y.append(j*0.05-1.0)
#5 second video at 30 fps = 150 frames
for n in range(150):
    plt.close('all')
    fig, axes = plt.subplots(figsize=(5,5))
    #adding the Circles to the plot using a PatchCollection is much faster.
    patches=[]
    #some cool motion
    for i in range(len(x)):
        x[i]+=0.001*cos(x[i]+3*y[i])
        y[i]+=0.001*sin(6*x[i]-4*y[i])
        patches.append(plt.Circle((x[i],y[i]),0.015))
    axes.add_collection(PatchCollection(patches, alpha=0.95))
    axes.set_xlim(-1.4,1.4)
    axes.set_ylim(-1.4,1.4)
    axes.set_aspect('equal')
    plt.savefig("out/fig%03d.png"%n,dpi=80)

The result:

enter image description here

Upvotes: 4

Related Questions