首页 > 解决方案 > 如何平滑类似于 d3 的 curveCardinal 方法实现的闭合线的边缘?

问题描述

我有一些使用闭合线图连接的数据点,我希望线条具有平滑的边缘,类似于 d3 中的curveCardinal方法如何做到这一点。链接在这里

这是我想要做的一个最小的例子:

import numpy as np
from matplotlib import pyplot as plt

x = np.array([0.5, 0.13, 0.4, 0.5, 0.6, 0.7, 0.5])
y = np.array([1.0, 0.7, 0.5, 0.2, 0.4, 0.6, 1.0])

fig, ax = plt.subplots()

ax.plot(x, y)
ax.scatter(x, y)

现在,我想平滑/插入类似于 d3 的 curveCardinal 方法的线。这是我尝试过的一些事情。

from scipy import interpolate

tck, u = interpolate.splprep([x, y], s=0, per=True)  
xi, yi = interpolate.splev(np.linspace(0, 1, 100), tck)

fig, ax = plt.subplots(1, 1)

ax.plot(xi, yi, '-b')
ax.plot(x, y, 'k')
ax.scatter(x[:2], y[:2], s=200)
ax.scatter(x, y)

上面代码的结果还不错,但是我希望当数据点相距很远时曲线会更靠近直线(我在上面增加了两个这样的数据点的大小以突出显示这一点)。本质上,让曲线靠近直线。

使用 interp1d (与上面的代码有相同的问题):

from scipy.interpolate import interp1d

x = [0.5, 0.13, 0.4, 0.5, 0.6, 0.7, 0.5]
y = [1.0, 0.7, 0.5, 0.2, 0.4, 0.6, 1.0]

orig_len = len(x)
x = x[-3:-1] + x + x[1:3]
y = y[-3:-1] + y + y[1:3]

t = np.arange(len(x))
ti = np.linspace(2, orig_len + 1, 10 * orig_len)

kind='cubic'
xi = interp1d(t, x, kind=kind)(ti)
yi = interp1d(t, y, kind=kind)(ti)

fig, ax = plt.subplots()
ax.plot(xi, yi, 'g')
ax.plot(x, y, 'k')
ax.scatter(x, y)

我还查看了 Chaikins Corner Cutting 算法,但我不喜欢这个结果。

def chaikins_corner_cutting(coords, refinements=5):
    coords = np.array(coords)

    for _ in range(refinements):
        L = coords.repeat(2, axis=0)
        R = np.empty_like(L)
        R[0] = L[0]
        R[2::2] = L[1:-1:2]
        R[1:-1:2] = L[2::2]
        R[-1] = L[-1]
        coords = L * 0.75 + R * 0.25

    return coords


fig, ax = plt.subplots()

ax.plot(x, y, 'k', linewidth=1)
ax.plot(chaikins_corner_cutting(x, 4), chaikins_corner_cutting(y, 4))

表面上,我还查看了 Bezier 曲线、matplotlibs PathPatch 和 Fancy box 实现,但我没有得到任何令人满意的结果。

非常感谢您的建议。

标签: pythonmatplotlibd3.jsscipy

解决方案


所以,这就是我最终这样做的方式。我决定在每两个现有数据点之间引入新点。下图显示了我如何添加这些新点。红色是我拥有的数据。使用凸包计算数据点的几何中心并从每个点画线(用蓝线显示)。将这些线分成两半并连接结果点(绿线)。绿线的中心是添加的新点。

在此处输入图像描述

以下是完成此操作的函数:

def midpoint(p1, p2, sf=1):
    """Calculate the midpoint, with an optional 
    scaling-factor (sf)"""
    xm = ((p1[0]+p2[0])/2) * sf
    ym = ((p1[1]+p2[1])/2) * sf
    return (xm, ym)
def star_curv(old_x, old_y):
    """ Interpolates every point by a star-shaped curve. It does so by adding
    "fake" data points in-between every two data points, and pushes these "fake"
    points towards the center of the graph (roughly 1/4 of the way).
    """

    try:
        points = np.array([old_x, old_y]).reshape(7, 2)
        hull = ConvexHull(points)
        x_mid = np.mean(hull.points[hull.vertices,0])
        y_mid = np.mean(hull.points[hull.vertices,1])
    except:
        x_mid = 0.5
        y_mid = 0.5

    c=1
    x, y = [], []
    for i, j in zip(old_x, old_y):
        x.append(i)
        y.append(j)
        try:
            xm_i, ym_i = midpoint((i, j),
                                  midpoint((i, j), (x_mid, y_mid)))

            xm_j, ym_j = midpoint((old_x[c], old_y[c]),
                                  midpoint((old_x[c], old_y[c]), (x_mid, y_mid)))

            xm, ym = midpoint((xm_i, ym_i), (xm_j, ym_j))
            x.append(xm)
            y.append(ym)
            c += 1
        except IndexError:
            break


    orig_len = len(x)
    x = x[-3:-1] + x + x[1:3]
    y = y[-3:-1] + y + y[1:3]


    t = np.arange(len(x))
    ti = np.linspace(2, orig_len + 1, 10 * orig_len)

    kind='quadratic'
    xi = interp1d(t, x, kind=kind)(ti)
    yi = interp1d(t, y, kind=kind)(ti)

    return xi, yi

这是它的外观:

import numpy as np
import matplotlib.pyplot as plt
from scipy.interpolate import interp1d
from scipy.spatial import ConvexHull

x = [0.5, 0.13, 0.4, 0.5, 0.6, 0.7, 0.5]
y = [1.0, 0.7, 0.5, 0.2, 0.4, 0.6, 1.0]

xi, yi = star_curv(x, y)

fig, ax = plt.subplots()
ax.plot(xi, yi, 'g')

ax.plot(x, y, 'k', alpha=0.5)
ax.scatter(x, y, color='r')

在此处输入图像描述

当数据点更加对称时,结果尤其明显,例如以下 x、y 值给出了下图中的结果:

x = [0.5, 0.32, 0.34, 0.5, 0.66, 0.65, 0.5]
y = [0.71, 0.6, 0.41, 0.3, 0.41, 0.59, 0.71]

此处介绍的插值与默认 interp1d 插值之间的比较。

在此处输入图像描述


推荐阅读