Path 'contains_points' gives incorrect results with Bezier curve
I'm trying to select a data area based on a matplotlib object Path
, but when the path contains a Bezier curve (not just straight lines) the selected area does not completely fill the curve. He looks like he's trying, but the far side of the curve is chopped off.
For example, the following code defines a fairly simple closed path with one straight line and one cubic curve. When I look at the True / False result from the method contains_points
, it seems to match neither the curve itself nor the raw vertices.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.path import Path
from matplotlib.patches import PathPatch
# Make the Path
verts = [(1.0, 1.5), (-2.0, 0.25), (-1.0, 0.0), (1.0, 0.5), (1.0, 1.5)]
codes = [Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.CURVE4, Path.CLOSEPOLY]
path1 = Path(verts, codes)
# Make a field with points to select
nx, ny = 101, 51
x = np.linspace(-2, 2, nx)
y = np.linspace(0, 2, ny)
yy, xx = np.meshgrid(y, x)
pts = np.column_stack((xx.ravel(), yy.ravel()))
# Construct a True/False array of contained points
tf = path1.contains_points(pts).reshape(nx, ny)
# Make a PathPatch for display
patch1 = PathPatch(path1, facecolor='c', edgecolor='b', lw=2, alpha=0.5)
# Plot the true/false array, the patch, and the vertices
fig, ax = plt.subplots()
ax.imshow(tf.T, origin='lower', extent=(x[0], x[-1], y[0], y[-1]))
ax.add_patch(patch1)
ax.plot(*zip(*verts), 'ro-')
plt.show()
It gives me this plot:
It looks like there is some kind of approximation going on - is this just a fundamental limitation of the calculation in matplotlib, or am I doing something wrong?
I can figure out the points inside the curve myself, but I was hoping not to reinvent this wheel if I didn't need it.
It's worth noting that the simpler quadratic curve construction actually works correctly:
I am using matplotlib 2.0.0.
source to share
This has to do with the space in which paths are evaluated, as described in GitHub release # 6076 . From mdboom's comment:
Intersecting paths is done by converting curves to line segments and then converting the intersection based on line segments. This transformation is done by "sampling" the curve with a 1.0 step. This is usually the right thing to do when paths are already scaled in display space, because sampling a curve at a lower resolution than one pixel doesn't really help. However, when calculating the intersection in data space as you did here, we obviously need to sample at a finer resolution.
Intersections are discussed here, but also influences contains_points
. This improvement is still open, so we'll need to see if it gets reviewed at the next milestone. At the same time, there are several options:
1) If you are going to display the patch anyway, you can use display transform. In the example above, adding the following demonstrates the correct behavior (based on tacaswell's comment on duplicate issue # 8734 , now closed):
# Work in transformed (pixel) coordinates
hit_patch = path1.transformed(ax.transData)
tf1 = hit_patch.contains_points(ax.transData.transform(pts)).reshape(nx, ny)
ax.imshow(tf2.T, origin='lower', extent=(x[0], x[-1], y[0], y[-1]))
2) If you are not using a display and just want to compute with a path, your best bet is to simply shape a Bezier curve and make a path from line segments. Replacing the shaping with the path1
following calculation path2
will produce the desired result.
from scipy.special import binom
def bernstein(n, i, x):
coeff = binom(n, i)
return coeff * (1-x)**(n-i) * x**i
def bezier(ctrlpts, nseg):
x = np.linspace(0, 1, nseg)
outpts = np.zeros((nseg, 2))
n = len(ctrlpts)-1
for i, point in enumerate(ctrlpts):
outpts[:,0] += bernstein(n, i, x) * point[0]
outpts[:,1] += bernstein(n, i, x) * point[1]
return outpts
verts1 = [(1.0, 1.5), (-2.0, 0.25), (-1.0, 0.0), (1.0, 0.5), (1.0, 1.5)]
nsegments = 31
verts2 = np.concatenate([bezier(verts1[:4], nsegments), np.array([verts1[4]])])
codes2 = [Path.MOVETO] + [Path.LINETO]*(nsegments-1) + [Path.CLOSEPOLY]
path2 = Path(verts2, codes2)
Either method gives something similar to the following:
source to share