Аппроксимация данных с помощью многосегментной кубической кривой Безье и расстояния, а также ограничения кривизны

У меня есть некоторые геоданные (на изображении ниже путь реки показан красными точками), которые я хочу аппроксимировать, используя многосегментную кубическую кривую Безье. По другим вопросам о stackoverflow здесь и здесь Я нашел алгоритм Филиппа Шнайдера из "Graphics Gems" . Я успешно реализовал его и могу сообщить, что даже с тысячами баллов это очень быстро. К сожалению, такая скорость имеет ряд недостатков, а именно то, что установка выполняется довольно небрежно. Рассмотрим следующий рисунок:

многосегментная кривая Безье

Красные точки - это мои исходные данные, а синяя линия - многосегментный график Безье, созданный алгоритмом Шнайдера. Как видите, входными данными для алгоритма был допуск, который, по крайней мере, равен значению, указанному зеленой линией. Тем не менее алгоритм создает кривую Безье, имеющую слишком много крутых поворотов. Вы также видите эти ненужные резкие повороты на изображении. Легко представить кривую Безье с менее резкими поворотами для показанных данных, при этом сохраняя условие максимального допуска (просто сдвиньте кривую Безье немного в направлении пурпурных стрелок). Проблема, похоже, в том, что алгоритм выбирает точки данных из моих исходных данных в качестве конечных точек отдельных кривых Безье (точки пурпурных стрелок указывают на некоторых подозреваемых). Поскольку конечные точки кривых Безье ограничены таким образом, ясно, что алгоритм иногда дает довольно резкие искривления.

Я ищу алгоритм, который аппроксимирует мои данные многосегментной кривой Безье с двумя ограничениями:

  • многосегментная кривая Безье никогда не должна находиться на расстоянии более определенного расстояния от точек данных (это обеспечивается алгоритмом Шнайдера)
  • многосегментная кривая Безье никогда не должна создавать слишком резкие кривизны. Один из способов проверить соответствие этим критериям - прокрутить круг с минимальным радиусом кривизны вдоль многосегментной кривой Безье и проверить, касается ли он всех частей кривой на своем пути. Хотя кажется, что есть лучший метод с использованием произведение первой и второй производной

К сожалению, решения, которые я нашел, которые лучше подходят, либо работают только для одиночных кривых Безье (и опускают вопрос о том, как найти хорошие начальные и конечные точки для каждой кривой Безье на многосегментной кривой Безье), либо не допускают минимального ограничения кривизны. Я считаю, что ограничение минимальной кривизны здесь - сложное условие.

Вот еще один пример (он нарисован от руки и не на 100% точен):

несколько примеров

Предположим, что на рисунке 1 показаны как ограничение кривизны (круг должен соответствовать всей кривой), так и максимальное расстояние любой точки данных от кривой (которое оказывается радиусом круга зеленого цвета). Успешное приближение красного пути на рисунке 2 показано синим цветом. Это приближение учитывает условие кривизны (круг может катиться внутри всей кривой и касаться ее повсюду), а также условие расстояния (показано зеленым). На рисунке 3 показан путь в другом приближении. Хотя здесь соблюдается условие расстояния, ясно, что круг больше не вписывается в кривизну. На рисунке 4 показан путь, который невозможно приблизить с данными ограничениями, потому что он слишком острый. Этот пример должен проиллюстрировать, что для правильной аппроксимации некоторых острых поворотов на пути необходимо, чтобы алгоритм выбирал контрольные точки, которые не являются частью пути. На рисунке 3 показано, что если были выбраны контрольные точки вдоль пути, ограничение кривизны больше не может выполняться. Этот пример также показывает, что алгоритм должен завершиться на некоторых входах, поскольку его невозможно аппроксимировать с заданными ограничениями.

Есть ли решение этой проблемы? Решение не должно быть быстрым. Если на обработку 1000 точек уходит день, ничего страшного. Решение также не должно быть оптимальным в том смысле, что оно должно приводить к аппроксимации методом наименьших квадратов.

В конце концов, я реализую это на C и Python, но я могу читать и на большинстве других языков.


person josch    schedule 21.03.2014    source источник
comment
Это оправдывает очень важный вопрос: хотите ли вы использовать кривую Безье или действительно хотите подогнать функцию под свои данные? Потому что вам определенно не нужна кривая Безье, если вы хотите заниматься хорошей наукой.   -  person Mike 'Pomax' Kamermans    schedule 22.03.2014
comment
Функции @ Mike'Pomax'Kamermans связывают каждый вход ровно с одним выходом. Как вы можете видеть выше, значение x может иметь несколько значений y. Как там функция может помочь?   -  person josch    schedule 22.03.2014
comment
Точно так же, как вы используете функцию Безье. Если вы хотите точного изображения реки, использовать поли-Безье немного странно. Вам нужны кривые через точки, поэтому, по крайней мере, мы рассматриваем возможность применения вместо них кривых Катмулла-Рома (забавный факт: оба являются сплайнами Эрмита и представляют друг друга, за исключением того, что кривые Катмулла-Рома проходят через точки с определенной скоростью: в значительной степени именно то, что вам нужно для моделирования речных данных)   -  person Mike 'Pomax' Kamermans    schedule 22.03.2014
comment
Я не хочу кривых через точки. Как я отметил в своем вопросе, алгоритм Шнайдера дает результаты, которых я не хочу, по кривой, проходящей через некоторые точки. Я хочу приблизиться к реке, а не проследить ее точно. Используя сплайны, я повторно использую имеющиеся у меня точки в качестве контрольных. Я не хочу этого. Я хочу гладкое приближение.   -  person josch    schedule 22.03.2014
comment
Достаточно справедливо, но именно здесь вы попадаете в субъективные требования. Ваш пост фактически не формализует их, требования остаются расплывчатыми: большее определенное расстояние, слишком резкие изгибы и т. Д. - это не то, с чем мы можем вам помочь, если вы не уточните. Что для вас такое расстояние, что значит "слишком резкое" и т. Д. У вас есть подробный вопрос, но он содержит неверные детали.   -  person Mike 'Pomax' Kamermans    schedule 22.03.2014
comment
определенное расстояние - это лишь один из входных параметров алгоритма. Это максимальное расстояние каждой входной точки от кривой в нормальном направлении. И я объяснил, что такое слишком острый, на примере катания по нему сферы с определенным радиусом (представляющим минимальную кривизну).   -  person josch    schedule 22.03.2014
comment
Возможно, вы сможете воспользоваться тем фактом, что сегмент Безье всегда находится внутри трапеции, образованной его 4 контрольными точками.   -  person Mark Ransom    schedule 01.04.2014


Ответы (3)


Я нашел решение, которое соответствует моим критериям. Решение состоит в том, чтобы сначала найти B-сплайн, который аппроксимирует точки по методу наименьших квадратов, а затем преобразовать этот сплайн в многосегментную кривую Безье. B-сплайны имеют то преимущество, что в отличие от кривых Безье они не проходят через контрольные точки, а также предоставляют способ задать желаемую «гладкость» аппроксимационной кривой. Необходимая функциональность для создания такого сплайна реализована в библиотеке FITPACK, к которой scipy предлагает привязку python. Предположим, я прочитал свои данные в списках x и y, тогда я могу:

import matplotlib.pyplot as plt
import numpy as np
from scipy import interpolate
tck,u = interpolate.splprep([x,y],s=3)
unew = np.arange(0,1.01,0.01)
out = interpolate.splev(unew,tck)
plt.figure()
plt.plot(x,y,out[0],out[1])
plt.show()

Результат выглядит так:

введите описание изображения здесь

Если я хочу, чтобы кривая была более плавной, я могу увеличить параметр s до splprep. Если я хочу, чтобы приближение было ближе к данным, я могу уменьшить параметр s для меньшей плавности. Программно перебирая несколько s параметров, я могу найти хороший параметр, который соответствует заданным требованиям.

Однако вопрос в том, как преобразовать этот результат в кривую Безье. Ответ в этом письме Захари Пинкуса. Я воспроизведу его решение здесь, чтобы дать исчерпывающий ответ на мой вопрос:

def b_spline_to_bezier_series(tck, per = False):
  """Convert a parametric b-spline into a sequence of Bezier curves of the same degree.

  Inputs:
    tck : (t,c,k) tuple of b-spline knots, coefficients, and degree returned by splprep.
    per : if tck was created as a periodic spline, per *must* be true, else per *must* be false.

  Output:
    A list of Bezier curves of degree k that is equivalent to the input spline. 
    Each Bezier curve is an array of shape (k+1,d) where d is the dimension of the
    space; thus the curve includes the starting point, the k-1 internal control 
    points, and the endpoint, where each point is of d dimensions.
  """
  from fitpack import insert
  from numpy import asarray, unique, split, sum
  t,c,k = tck
  t = asarray(t)
  try:
    c[0][0]
  except:
    # I can't figure out a simple way to convert nonparametric splines to 
    # parametric splines. Oh well.
    raise TypeError("Only parametric b-splines are supported.")
  new_tck = tck
  if per:
    # ignore the leading and trailing k knots that exist to enforce periodicity 
    knots_to_consider = unique(t[k:-k])
  else:
    # the first and last k+1 knots are identical in the non-periodic case, so
    # no need to consider them when increasing the knot multiplicities below
    knots_to_consider = unique(t[k+1:-k-1])
  # For each unique knot, bring it's multiplicity up to the next multiple of k+1
  # This removes all continuity constraints between each of the original knots, 
  # creating a set of independent Bezier curves.
  desired_multiplicity = k+1
  for x in knots_to_consider:
    current_multiplicity = sum(t == x)
    remainder = current_multiplicity%desired_multiplicity
    if remainder != 0:
      # add enough knots to bring the current multiplicity up to the desired multiplicity
      number_to_insert = desired_multiplicity - remainder
      new_tck = insert(x, new_tck, number_to_insert, per)
  tt,cc,kk = new_tck
  # strip off the last k+1 knots, as they are redundant after knot insertion
  bezier_points = numpy.transpose(cc)[:-desired_multiplicity]
  if per:
    # again, ignore the leading and trailing k knots
    bezier_points = bezier_points[k:-k]
  # group the points into the desired bezier curves
  return split(bezier_points, len(bezier_points) / desired_multiplicity, axis = 0)

Так что B-Splines, FITPACK, numpy и scipy спасли мне день :)

person josch    schedule 29.03.2014
comment
В вашем исходном сообщении указано, что желаемый алгоритм должен иметь возможность принимать ограничения кривизны. Однако я не видел таких ограничений при использовании FITPACK в ваших решениях. - person fang; 21.12.2014
comment
@fang параметр гладкости кажется достаточным для контроля кривизны в моем случае использования. Чем больше s, тем менее резкими будут кривые. - person josch; 22.12.2014
comment
OK. В вашем исходном посте говорится об использовании круга с минимальным радиусом кривизны r, чтобы проверить, есть ли у сплайна крутые повороты. Это заставляет меня думать, что вы хотели бы иметь количественный контроль кривизны сплайна (а именно, вы хотите, чтобы максимальная кривизна сплайна была меньше заданного значения). Если качественный контроль кривизны вам подходит, действительно, упомянутый вами параметр подходит для этой цели. - person fang; 22.12.2014

  1. полигонизировать данные

    найдите порядок точек, чтобы вы просто находили самые близкие друг к другу точки и пытались соединить их «линиями». Избегайте возврата к исходной точке

  2. вычислить вывод по пути

    это изменение направления «линий», где вы нажимаете локальный минимум или максимум, это ваша контрольная точка ... Сделайте это, чтобы уменьшить ваши входные данные (оставьте только контрольные точки).

  3. кривая

    теперь используйте эти точки как контрольные. Я настоятельно рекомендую использовать полином интерполяции как для x, так и для y по отдельности, например, примерно так:

    x=a0+a1*t+a2*t*t+a3*t*t*t
    y=b0+b1*t+b2*t*t+b3*t*t*t
    

    где a0..a3 вычисляются следующим образом:

    d1=0.5*(p2.x-p0.x);
    d2=0.5*(p3.x-p1.x);
    a0=p1.x;
    a1=d1;
    a2=(3.0*(p2.x-p1.x))-(2.0*d1)-d2;
    a3=d1+d2+(2.0*(-p2.x+p1.x));
    
    • b0 .. b3 are computed in same way but use y coordinates of course
    • p0..p3 - контрольные точки для кривой кубической интерполяции
    • t =<0.0,1.0> - параметр кривой от p1 до p2

    это гарантирует, что позиция и первая деривация будут непрерывными (c1), а также вы можете использовать BEZIER, но это будет не так хорошо, как это.

[edit1] слишком острые края - БОЛЬШАЯ проблема

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

  1. удалить точки из набора данных со слишком высокой первой производной

    dx/dl или dy/dl, где x,y - координаты, а l - длина кривой (вдоль ее пути). Точное вычисление радиуса кривизны на основе построения кривой сложно.

  2. удалить точки из набора данных, которые приводят к слишком маленькому радиусу кривизны

    вычислить пересечение средней точки соседних сегментов линии (черные линии). Перпендикулярные оси, как на изображении (красные линии), расстояние между ними и точкой соединения (синяя линия) - это ваш радиус кривизны. Когда радиус кривизны меньше, то ваш предел удалите эту точку ...

    радиус кривизны

    теперь, если вам действительно нужны только кубики BEZIER, вы можете преобразовать мою кубическую интерполяцию в кубическую BEZIER следующим образом:

//  ---------------------------------------------------------------------------
//  x=cx[0]+(t*cx[1])+(tt*cx[2])+(ttt*cx[3]); // cubic x=f(t), t = <0,1>
//  ---------------------------------------------------------------------------
//  cubic matrix                           bz4 = it4
//  ---------------------------------------------------------------------------
//  cx[0]=                            (    x0) =                    (    X1)
//  cx[1]=                   (3.0*x1)-(3.0*x0) =           (0.5*X2)         -(0.5*X0)
//  cx[2]=          (3.0*x2)-(6.0*x1)+(3.0*x0) = -(0.5*X3)+(2.0*X2)-(2.5*X1)+(    X0)
//  cx[3]= (    x3)-(3.0*x2)+(3.0*x1)-(    x0) =  (0.5*X3)-(1.5*X2)+(1.5*X1)-(0.5*X0)
//  ---------------------------------------------------------------------------
    const double m=1.0/6.0;
    double x0,y0,x1,y1,x2,y2,x3,y3;
    x0 = X1;           y0 = Y1;
    x1 = X1-(X0-X2)*m; y1 = Y1-(Y0-Y2)*m;
    x2 = X2+(X1-X3)*m; y2 = Y2+(Y1-Y3)*m;
    x3 = X2;           y3 = Y2;

Если вам нужно обратное преобразование, см .:

person Spektre    schedule 22.03.2014
comment
Привет, спасибо за подробный ответ! Я не думаю, что это ответ на мой вопрос. Чтобы прояснить мой вопрос, я добавил еще один графический пример. Точнее, я не думаю, что ваше решение может учитывать ограничение кривизны, потому что оно выбирает точки пути в качестве контрольных точек для кривой. Мой пример показывает, что для некоторых очень острых путей необходимо выбирать контрольные точки, отличные от входного пути. - person josch; 24.03.2014
comment
@josch добавил [edit1]. кстати, кубическая интерполяция конвертируется в кубик Безье, но в форме интерполяции она намного более управляема ... - person Spektre; 24.03.2014
comment
Благодарность! это невероятно полезный ответ, я вернусь после того, как опробую ваши предложения - person josch; 24.03.2014
comment
Я только что наткнулся на этот пост и заметил, что положение оператора и первое вывод непрерывно (c2). Это не так. Если кривая непрерывна только в первой производной, это будет только C1. Непрерывность C2 требует, чтобы вторые производные были непрерывными. - person fang; 21.12.2014
comment
@fang по-английски кодируется от C0? Я видел обе кодировки из C0 и из C1 в литературе (не на английском языке), поэтому уверены ли вы, что это должно быть из C0 (поэтому я всегда указываю переменные, которые являются непрерывными)? в таком случае отредактирую ... - person Spektre; 22.12.2014
comment
@Spektre: Я уверен, что это C0 для позиционной непрерывности, C1 для непрерывности по первой производной и C2 для непрерывности по второй производной для всей английской литературы, которую я видел. Тем не менее, я должен признать, что не знаю, есть ли какие-нибудь неанглоязычные литературы, которые придерживаются другой конвенции. - person fang; 22.12.2014

Вопрос был опубликован давно, но вот простое решение, основанное на splprep, нахождение минимального значения s, позволяющего выполнить критерии минимального радиуса кривизны.

route - это набор входных точек, первое измерение - это количество точек.

import numpy as np
from scipy.interpolate import splprep, splev

#The minimum curvature radius we want to enforce
minCurvatureConstraint = 2000
#Relative tolerance on the radius
relTol = 1.e-6

#Initial values for bisection search, should bound the solution
s_0 = 0
minCurvature_0 = 0
s_1 = 100000000 #Should be high enough to produce curvature radius larger than constraint
s_1 *= 2
minCurvature_1 = np.float('inf')

while np.abs(minCurvature_0 - minCurvature_1)>minCurvatureConstraint*relTol:
    s = 0.5 * (s_0 + s_1)
    tck, u  = splprep(np.transpose(route), s=s)
    smoothed_route = splev(u, tck)
    
    #Compute radius of curvature
    derivative1 = splev(u, tck, der=1)
    derivative2 = splev(u, tck, der=2) 
    xprim = derivative1[0]
    xprimprim = derivative2[0]
    yprim = derivative1[1]
    yprimprim = derivative2[1]
    curvature = 1.0 / np.abs((xprim*yprimprim - yprim* xprimprim) / np.power(xprim*xprim + yprim*yprim, 3 / 2))
    minCurvature = np.min(curvature)
    print("s is %g => Minimum curvature radius is %g"%(s,np.min(curvature)))

    #Perform bisection
    if minCurvature > minCurvatureConstraint:
        s_1 = s
        minCurvature_1 = minCurvature
    else:
        s_0 = s
        minCurvature_0 = minCurvature

Это может потребовать некоторых доработок, таких как итерации, чтобы найти подходящий s_1, но работает.

person seb007    schedule 13.01.2021