路径 'contains_points' 使用贝塞尔曲线产生不正确的结果

Path 'contains_points' yields incorrect results with Bezier curve

我正在尝试 select 基于 matplotlib Path 对象的数据区域,但是当路径包含贝塞尔曲线(不仅仅是直线)时,select ed 区域没有完全填满曲线。看起来它正在尝试,但曲线的远端被切断了。

例如,以下代码定义了一条相当简单的封闭路径,其中包含一条直线和一条三次曲线。当我查看 contains_points 方法的 True/False 结果时,它似乎与曲线本身或原始顶点都不匹配。

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()

这给了我这个情节:

看起来正在进行某种近似 - 这只是 matplotlib 中计算的基本限制,还是我做错了什么?

可以自己计算曲线内的点,但如果没有必要,我希望不要重新发明这个轮子。

值得注意的是,使用二次曲线的更简单的构造似乎可以正常工作:

我正在使用 matplotlib 2.0.0.

这与评估路径的 space 有关,如 GitHub 问题 #6076 中所述。来自 mdboom 的评论:

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.

这是在讨论路口,但contains_points也受到影响。此增强功能仍处于开放状态,因此我们必须看看它是否会在下一个里程碑中得到解决。同时,有几个选项:

1) 如果无论如何都要显示补丁,可以使用显示转换。在上面的示例中,添加以下内容演示了正确的行为(基于 tacaswell 对重复问题 #8734 的评论,现已关闭):

# 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) 如果您不使用显示器而只想使用路径进行计算,最好的办法是自己简单地形成贝塞尔曲线并制作一条路径的线段。将 path1 的形成替换为以下 path2 的计算将产生所需的结果。

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)

这两种方法都会产生如下所示的结果: