Как сгладить края замкнутой линии, аналогичной реализации метода d3's curveCardinal? - PullRequest
1 голос
/ 14 июня 2019

У меня есть несколько точек данных, которые я соединяю, используя график с закрытой линией, и я хочу, чтобы у линии были гладкие края, аналогично тому, как это делают методы curveCardinal в d3. Ссылка здесь

Вот минимальный пример того, что я хочу сделать:

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's 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)

Я также посмотрел на алгоритм обрезки углов Чайкина, но мне не нравится результат.

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

Я также поверхностно посмотрел на кривые Безье, matplotlibs PathPatch и реализации Fancy box, но я не смог получить удовлетворительных результатов.

Предложения приветствуются.

Ответы [ 2 ]

1 голос
/ 15 июня 2019

Итак, вот как я это сделал. Я решил ввести новые точки между каждыми двумя существующими точками данных. На следующем рисунке показано, как я добавляю эти новые точки. Красные - это данные, которые у меня есть. Используя выпуклую оболочку, я рассчитываю геометрический центр точек данных и рисую для него линии из каждой точки (показаны синими линиями). Разделите эти линии дважды пополам и соедините полученные точки (зеленая линия). В центре зеленой линии добавлена ​​новая точка.

enter image description here

Вот функции, которые выполняют это:

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

enter image description here

Результат особенно заметен, когда точки данных более симметричны, например, следующие значения 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.

enter image description here

0 голосов
/ 15 июня 2019

Я бы создал другой массив с вершинами, расширенными в / из или вверх / вниз примерно на 5%.Поэтому, если точка ниже среднего значения соседних точек, сделайте ее еще немного ниже.

Затем выполните линейную интерполяцию между новыми точками, скажем, 10 точек на ребро.Наконец, сделайте сплайн между второй последней точкой на ребре и фактической вершиной.Если вы используете кривые Безье, вы можете заставить сплайн входить под одинаковым углом с каждой стороны.

Это немного трудоемко, но, конечно, вы можете использовать это где угодно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...