简体   繁体   中英

Path 'contains_points' yields incorrect results with Bezier curve

I am trying to select a region of data based on a matplotlib Path object, but when the path contains a Bezier curve (not just straight lines), the selected region doesn't completely fill in the curve. It looks like it's trying, but the far side of the curve gets 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 contains_points method, it does not seem to match either the curve itself or 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()

This gives me this plot:

真实点不能完全填满曲线

It looks like there is some sort of approximation going on - is this just a fundamental limitation of the calculation in matplotlib, or am I doing something wrong?

I can calculate the points inside the curve myself, but I was hoping to not reinvent this wheel if I don't have to.

It's worth noting that a simpler construction using quadratic curves does appear to work properly:

模拟圆的二次曲线工作正常

I am using matplotlib 2.0.0.

This has to do with the space in which the paths are evaluated, as explained in GitHub issue #6076 . From a comment by mdboom there:

Path intersection is done by converting the curves to line segments and then converting the intersection based on the line segments. This conversion happens by "sampling" the curve at increments of 1.0. This is generally the right thing to do when the paths are already scaled in display space, because sampling the curve at a resolution finer than a single pixel doesn't really help. However, when calculating the intersection in data space as you've done here, we obviously need to sample at a finer resolution.

This is discussing intersections, but contains_points is also affected. This enhancement is still open so we'll have to see if it is addressed in the next milestone. In the meantime, there are a couple options:

1) If you are going to be displaying a patch anyway, you can use the display transformation. In the example above, adding the following demonstrates the correct behavior (based on a comment by tacaswell 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 aren't using a display and just want to calculate using a path, the best bet is to simply form the Bezier curve yourself and make a path out of line segments. Replacing the formation of path1 with the following calculation of 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 yields something that looks like the following:

真假匹配贝塞尔曲线

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM