Кривые Безье.Применение

Базовые сведения

Как GDI, так и GDI+ предоставляют функции для рисования кривых Безье. При этом используются кубические кривые, для построения которых нужны четыре опорные точки. Простой пример для GDI+ построения кривой и ее опорных точек:

pascal
var g : TGPGraphics;
    pen : TGPPen;
    brush : TGPBrush;
    P : array [1..4] of TGPPointF;
    i : integer;
begin
   g := TGPGraphics.Create(image1.Canvas.Handle);
   pen := TGPPen.Create(aclBlack, 2);
   brush := TGPSolidBrush.Create(aclGreen);
   try
      g.SetSmoothingMode(SmoothingModeAntiAlias);

      P[1] := makePoint(  10.0, 100);
      P[2] := makePoint(  60.0,  20);
      P[3] := makePoint( 110.0, 180);
      P[4] := makePoint( 160.0, 100);

      g.DrawBezier(pen, P[1], P[2], P[3], P[4]);

      pen.SetWidth(1);
      pen.SetColor(aclBlue);
      g.DrawLine(pen, P[1], P[2]);
      g.DrawLine(pen, P[3], P[4]);

      for i := 1 to 4 do
         g.FillEllipse(brush, P[i].x-3, P[i].y-3, 6, 6);
   finally
      brush.free;
      pen.free;
      g.free;
   end;



Вычисление точек кривой Безье

Кривая Безье описывается простым параметрическим уравнением:

 $$ b(t) = (1-t)^3p_0 + 3t(1-t)^2p_1 + 3t^2(1-t)p_2+t^3p_3, \quad t \in [0 \dotsc 1] $$

comment
Вообще, семейство кривых описывается следующим рядом формул:

 $$ b(t) = 1(s^1 t^0)P_0 + 1(s^0 t^1)P_1 $$ Линейные
 $$ b(t) = 1(s^2 t^0)P_0 + 2(s^1 t^1)P_1 + 1(s^0 t^2)P_2 $$ Квадратичные
 $$ b(t) = 1(s^3 t^0)P_0 + 3(s^2 t^1)P_1 + 3(s^1 t^2)P_2 + 1(s^0 t^3)P_3 $$ Кубические
 … (и т.д.)

Здесь $ P_i $ - опорные точки, коэфициенты - значения из треугольника Паскаля (или биномиальные коэффициенты). $ s $ и $ t $ - параметры кривых. Если принять $ s = 1-t $ мы получаем знакомую нам формулу.

Собственно нас интересуют кубические кривые (кстати, квадратичные легко преобразуются к кубическим и наоборот).

pascal
function BCoord(x0, x1, x2, x3, t:Single):Single;
begin
   result := (1-t)*(1-t)*(1-t)*x0+
             3*t*(1-t)*(1-t)*x1+
             3*t*t*(1-t)*x2+
             t*t*t*x3;
end;

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

pascal
var g : TGPGraphics;
    pen : TGPPen;
    brush : TGPBrush;
    P : array [1..4] of TGPPointF;
    t, x, y : Single;
    R1, R2:TGPPointF;
begin
   g := TGPGraphics.Create(image1.Canvas.Handle);
   pen := TGPPen.Create(aclBlack, 2);
   brush := TGPSolidBrush.Create(aclGreen);
   try
      g.SetSmoothingMode(SmoothingModeAntiAlias);

      P[1] := makePoint(  10.0, 100);
      P[2] := makePoint(  60.0,  20);
      P[3] := makePoint( 110.0, 180);
      P[4] := makePoint( 160.0, 100);

      g.DrawBezier(pen, P[1], P[2], P[3], P[4]);

      R1 := P[1];
      R2 := P[4];
      t := 0.125;
      while t < 1.0 do begin
         x := BCoord(P[1].x, P[2].x, P[3].x, P[4].x, t);
         y := BCoord(P[1].y, P[2].y, P[3].y, P[4].y, t);
         g.FillEllipse(brush, x-3, y-3, 6, 6);
         if x < R1.x then R1.x := x;
         if y < R1.y then R1.y := y;
         if x > R2.x then R2.x := x;
         if y > R2.y then R2.y := y;
         t := t + 0.125;
      end;

      pen.SetWidth(1);
      pen.SetColor(aclBlue);
      g.DrawRectangle(pen, 
             R1.x-2,      R1.y-2, 
             R2.x-R1.x+4, R2.Y-R1.Y+4);
   //...



Вычисление опорных точек.

Пусть изместно, что кривая выходит из точки A, проходит через точку B при t = 1/3, через точку C при t = 2/3 и заканчивается в точке D. Подставив эти значения в параметрическое уравнение кривой Безье получим выражения для вычисления координат опорных точек:

 $$ p_1 = (-5A+18B-9C+2D)/6; $$
 $$ p_2 = (2A-9B+18C-5D)/6; $$

Оформим эти вычисления отдельной процедурой. Она принимает четыре точки, через которые должна пройти кривая. В этих же переменных возвращаются начальная, конечная и две опорные точки кривой.

pascal
procedure ToBezier(var A, B, C, D:TGPoint);

 function Point1(x1, x2, x3, x4:Single):Single;
 begin
    result := (-5*x1+18*x2-9*x3+2*x4)/6;
 end;

 function Point2(x1, x2, x3, x4:Single):Single;
 begin
    result := (2*x1-9*x2+18*x3-5*x4)/6;
 end;

var B0, C0 : TGPoint;
begin
   B0.x := Point1(A.x, B.x, C.x, D.x);
   B0.y := Point1(A.y, B.y, C.y, D.y);
   C0.x := Point2(A.x, B.x, C.x, D.x);
   C0.y := Point2(A.y, B.y, C.y, D.y);
   B := B0;
   C := C0;
end;

Посмотрим как это работает:

pascal
var g : TGPGraphics;
    pen : TGPPen;
    brush : TGPSolidBrush;
    P : array [1..4] of TGPPointF;
    i : integer;
begin
   g := TGPGraphics.Create(image1.Canvas.Handle);
   pen := TGPPen.Create(aclBlack, 2);
   brush := TGPSolidBrush.Create(aclGreen);
   try
      g.SetSmoothingMode(SmoothingModeAntiAlias);

      P[1] := makePoint(  10.0, 100);
      P[2] := makePoint( 100.0, 150);
      P[3] := makePoint( 180.0, 120);
      P[4] := makePoint( 190.0,  20);

      for i := 1 to 4 do
         g.FillEllipse(brush, P[i].x-3, P[i].y-3, 6, 6);

      ToBezier(P[1], P[2], P[3], P[4]);
      g.DrawBezier(pen, P[1], P[2], P[3], P[4]);

      brush.SetColor(aclRed);
      for i := 2 to 3 do
         g.FillEllipse(brush, P[i].x-3, P[i].y-3, 6, 6);

      pen.SetWidth(1);
      pen.SetColor(aclBlue);
      for i := 1 to 3 do
         g.DrawLine(pen, P[i], P[i+1]);



Аппроксимация дуги эллипса кривыми Безье

Пусть задан эллипс с центром в точке (0, 0) и радиусами a, b (по оси X и по оси Y соответственно). Требуется построить дугу между заданными углами.

Прежде всего нужно найти точки начала и конца дуги - они же будут начальной и конечной точками кривой. Для заданного угла $ \alpha $ точку на эллипсе можно найти подставив уравнение прямой $ y = \tan(\alpha) x $ в уравнение эллипса. После несложных преобразований получаем:

 $$ x{}^2 = (a{}^2 b{}^2) / (a{}^2 \tan(\alpha){}^2 + b{}^2) $$

pascal
function GetArcPoint(f, a, b:Single):TGPoint;
var n:integer;
    t, x, y, t2, a2, b2:Single;
begin
   n := GetQuadransRad(f);

   if abs(f-pi/2) < 0.001 then begin
      x := 0; y := b;
   end else begin
      t := tan(f);
      t2 := t*t;
      b2 := b*b;
      a2 := a*a;
      x := sqrt((a2*b2) / (a2*t2+b2));
      y := x*t;
   end;
   case n of
      1 : begin x := +x; y := +y; end;
      2 : begin x := -x; y := +y; end;
      3 : begin x := -x; y := -y; end;
      4 : begin x := +x; y := -y; end;
   end;
   result.x := x;
   result.y := y;
end;

Здесь вызов GetQuadransRad определяет в каком квадранте находятся точки, лежащие на луче, проведенном от центар координат под углом f. Кроме того, она корректирует угол так, чтобы он лежал в первом квадранте (так удобнее проводить вычисления).

pascal
function GetQuadransRad(var a:Single):integer;
var n:integer;
begin
   if a > pi  then a := a - 2*pi;
   if a < -pi then a := a + 2*pi;

   n := 0;
   if a > 0 then begin
      if (a >=  0) and (a <= pi/2)
      then n := 1
      else
      if (a >  pi/2) and (a <= pi) then begin
         n := 2; a := pi-a;
      end
   end else begin
      if (a <=   0) and (a >= -pi/2) then begin
         n := 4; a := -a;
      end else
      if (a <  -pi/2) and (a >= -pi  ) then begin
         n := 3; a := pi+a;
      end
   end;
   result := n;
end;

Теперь мы можем вычислить две крайние точки дуги. После этого можно определить еще две точки дуги и по получившимся четырем точкам построить кривую Безье, вызвав уже реализованную ранее процедуру ToBezier.

pascal
procedure GetArcBezier(rx, ry, start, sweep:Single;
                  var A, B, C, D:TGPoint); 
var f : Single;
    t1, t2, t3, t4 :Single;
begin
   f := start*pi/180;
   A := GetArcPoint(f, rx, ry);

   f := (start+sweep)*pi/180;
   D := GetArcPoint(f, rx, ry);

   t1 := arccos(A.x/rx); if A.y < 0 then t1 := -t1;
   t4 := arccos(D.x/rx); if D.y < 0 then t4 := -t4;
   if sweep > 0 then begin
      if t4 < t1 then t4 := t4+2*pi;
   end else begin
      if t4 > t1 then t4 := t4-2*pi;
   end;

   t2 := (t4-t1)/3 + t1;
   t3 := (t4-t1)/3*2 + t1;
   B.x := rx*cos(t2);
   B.y := ry*sin(t2);
   C.x := rx*cos(t3);
   C.y := ry*sin(t3);
   ToBezier(A, B, C, D);
end;

Посмотрим, что у нас получилось.



Здесь красный эллипс нарисован средствами GDI+ для сравнения. Видно, что аппроксимация маленьких дуг (черным) совпадает с эталоном. Большие дуги сильно отклоняются от эталонной кривой.


Для точной аппроксимации разобъем дугу на две - три части и для каждой построим свою кривую Безье:

pascal
function GetArcBezier(rx, ry, start, sweep:Single; 
    var Beziers : array of TGPoint):integer; overload;
var f:Single;
    i:integer;
begin
   result := 0;
   if abs(sweep) > 360 then sweep := 360;
   if abs(sweep) < 1 then  exit;

   f := abs(sweep);
   if      f > 270 then result := 4
   else if f > 180 then result := 3
   else if f >  90 then result := 2
   else                 result := 1;

   f := sweep / result;
   for i := 0 to result - 1 do begin
      GetArcBezier(rx, ry, start+f*i, f,
          Beziers[i*4],
          Beziers[i*4+1],
          Beziers[i*4+2],
          Beziers[i*4+3]);
   end;
end;

Теперь мы получаем полное совпадение. (пример использования в приложении).



Аппроксимация синусоиды кривыми Безье

Ограничимся интервалом от 0 до pi/2. Будем задавать синусоиду на этом интервале вектором. Начальная точка синусоиды в центре координат, конечную указывает заданный вектор. Для вычисления опорных точек можно использовать заранее вычисленные коэффициенты:

Здесь V - вектор, задающий направление. sin - задает отклонение кривой от вектора. true соответствует отклонению вправо, если смотреть по направлению вектора. A, B, C, D - опорные точки кривой Безье. Построим несколько участков синусоиды (исходный код примера в приложении):



Аппроксимация параболы кривыми Безье

Как известно, параболу можно построить по трем точкам. Возьмем, например, точку перегиба параболы и любую другую точку кривой. Тогда третью точку можно получить простым отражением. Мы приходим к той-же схеме, что и для синусоиды, т.е. используем вектор для задания двух точек.



Соединение точек плавной кривой в GDI+

Естественно, GDI+ имеет средства для соединения точек плавной кривой. Можно строить как замкнутые, так и не замкнутые кривые.



Если интересен пример, смотрите код в прикрепленном архиве. Нас же больше интересует сам принцип построения таких кривых.

-


Пусть, например, плавная кривая должна пройти через точки P0, P1, P2, т.е. кривая будет состоять из двух кривых Безье. Для плавного соединения этих кривых в точке P1 нужно чтобы три последовательных опорных точки A, P1, и B лежали на одной прямой. По сути для получения опорных точек нам нужно найти касательную к кривой в точке P1. Зная касательную и выбрав на ней точки A и B мы получим опорные точки для построения кривой Безье. Повторив этот расчет для каждой заданной точки мы получим серию опорных точек по которым сможем построить несколько кривых Безье. Вместе эти кривые называются сплайном. Т.к. мы рассматриваем кубические кривые Безье, то и сплайн называется кубическим Безье сплайном (cubic Bezier spline). Соответственно из квадратичных кривых Безье можно построить квадратичный Безье сплайн (quadratic Bezier spline).

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

Кубические сплайны Эрмита

Кубические сплайны Эрмита (Cubic Hermite Splines) - это почти то-же самое, что и кубические сплайны Безье. Можно рассматривать их просто как другой способ задания кривых Безье. Вместо указания опорных точек мы в каждой точке кривой задаем вектор. Направление и величина этого вектора определяют скорость с которой кривая будет отклоняться к заданной точке.

Например, на рисунке слева вид кривой определяется векторами U и V, заданными для точек A и D, а на рисунке справа та-же кривая задана опорными точками B и C. Для преобразования векторов к опорным точкам кривой Безье существуют простые формулы:

$ B = A + (U/3) $
$ C = D - (V/3) $

Обратное преобразование:

$ U = 3 (B - A) $
$ V = 3 (D - C) $

Сплайны Катмулла-Рома

Сплайны Катмулла-Рома (Catmull-Rom spline) можно рассматривать как вариант построения кубических сплайнов Эрмита.

-
Например, нужно получить вектор для точки D из последовательности A, D, E. Возьмем отрезок AE (соединяющий две соседние с точкой D точки) и перенесем его так, чтобы его центр совпал с точкой D. Половина этого отрезка и даст нам нужный вектор. Теперь остается по этому вектору получить опорные точки и построить кривую Безье.

Открытым остается вопрос что делать на концах последовательности. Если мы строим замкнутую кривую, то для первой точки списка соседней будет последняя и наоборот. Когда же мы строим не замкнутую кривую, то возможны варианты:

  • считать соседними те-же точки, что и для замкнутой кривой.
  • крайнюю точку считать соседней самой себе.
  • дополнить список еще двумя точками, которые нужны только для получения крайних кривых Безье.

pascal
function toCMSlines(Points:array of TGPoint; 
     closed:boolean=true; asClosed:boolean=true):ArrayOfPoint;
var C, i, CE, N1, N2, k:integer;
    P1, P2, P0, C0, B0, P0C:TGPoint;
begin
   k := 0;
   C := High(Points) - Low(Points) + 1;
   CE := C;
   if not closed then dec(CE);
   SetLength(result, CE * 3+1);

   for i := 0 to C-1 do begin
      P0 := Points[i];

      // Получаем предыдущую и следующую точки
      N1 := i-1;
      if N1 < 0 then
         if closed or asClosed then N1 := C-1 else N1 := i;

      N2 := i+1;
      if N2 > C-1 then
         if closed or asClosed then N2 := 0 else N2 := i;

      P1 := Points[N1];
      P2 := Points[N2];

      // Половина вектора (P1,P2)
      P2.X := (P2.X - P1.X)/2;
      P2.Y := (P2.Y - P1.Y)/2;

      // Опорные точки двух соседних кривых Безье
      C0.X := P0.X - P2.X/3;
      C0.Y := P0.Y - P2.Y/3;
      B0.X := P0.X + P2.X/3;
      B0.Y := P0.Y + P2.Y/3;

      if i = 0
      then P0C := C0
      else begin
         result[k] := C0; inc(k);
      end;
      result[k] := P0; inc(k);
      if (i < C-1) or closed then begin
         result[k] := B0; inc(k);
      end;
   end;
   if closed then begin
      result[k] := P0C; inc(k);
      result[k] := Points[0];
   end;
end;
Посмотрим на реализацию построения сплайна Катмулла-Рома. Функция toCMSlines по заданной последовательности точек вычисляет опорные точки кривых Безье. (Полный пример смотрите в прикрепленном архиве).

Пример замкнутой и незамкнутой кривой:

Сплайны Кочанека-Бартельса

Сплайны Кочанека-Бартельса (Kochanek-Bartels splines) также являются кубическими сплайнами Эрмита, но дополнены тремя параметрами. Параметры называются tension, bias и continuity (натяжение, скос/склонение, непрерывность) и изменяют вид касательной и, следовательно, всей кривой.

Формулы для получения двух векторов в каждой точке:

 $$ d_i = \frac {(1-t)(1+b)(1+c)}{2} (p_i - p_{i-1}) +  \frac {(1-t)(1-b)(1-c)}{2} (p_{i+1} - p_{i}) $$
 $$ d_{i+1} = \frac {(1-t)(1+b)(1-c)}{2} (p_i - p_{i-1}) +  \frac {(1-t)(1-b)(1+c)}{2} (p_{i+1} - p_{i}) $$

Если параметры равны нулю, мы получаем сплайн Катмулла-Рома.

Метод для вычисления точек кривых Безье по этим формулам (полный пример в архиве):

В теории значения параметров должны лежать в пределах [-1, 1]. Посмотрим как меняется вид кривой в зависимости от параметров:

параметрВид кривой для значений параметра 2, 1, 0, -1
t

b

c

Литература

13 марта 2010

Downloads
Тестовое приложение с кодом, приведенным в статье (bezier_1.zip) (132.3 Кб, просмотров: 2003 )

Comments
09.01.2011 12:31 Артем
 Здорово. С первого раза нашел то, что надо (первый раз не пришлось самому всё вычислять).
 
05.02.2012 09:42 Айзат
 большое спасибо, очень помогло

09.03.2017 06:54 Алексей
 Огромное спасибо!!! Долго не мог решить проблему с нахождением опорных точек в кривой Безье, теперь всё стало понятно.
 Ещё раз огромное спасибо.
 
19.09.2017 03:42 Костя
 Большое спасибо! Планировал на реализацию 1 день. Наткнулся на эту статью - справился за 1
 5 минут (переписал на C#).
Вы можете оставить комментарий или задать вопрос
Ваше имя:

Текст сообщения:


Copyright © 2009-2014 by