与えられた点によって定まる滑らかな曲線の描き方


 以前、菊の葉 を描いたときは、葉の縁上の点を与え、それらを線分で結んで葉を描いた。しかし、これはいかにも点を線分で結びましたというような角張った葉であった。できれば、葉の縁はもっと滑らかな曲線で描きたい。
 ここでは、与えられた点によって定まる滑らかな曲線や、与えられた点を通るような滑らかな曲線を描く方法について学ぶ。1つは、点列によって形が定まるが必ずしもそれらの点を通るわけではない「ベジエ曲線」であり、もう一つは与えられた各点を通る「スプライン曲線」である。
  1. ベジエ曲線

     2点が与えられたとき、それらの点における接線を与えて、これら接線を滑らかに結ぶ曲線を数学的に表現できると都合が良い。この考えを、フランスの自動車メーカー ルノー社の技術者であった P.Bezier (ピエール・ベジエ)は車体の形状の設計に初めて適用したので、今日、このような曲線はベジエ曲線と呼ばれている(ただし、シトロエン社のド・カステリョ P.de Casteljau が独立に先んじて研究していたらしい)。
     ベジエ曲線は、n 個の点 P0, P1, ..., Pn-1 (制御点と呼ばれる)が与えられたとき、
           n-1                 n-1
        P(t) = ΣCiti(1-t)n-i-1Pi = Σwi(t)Pi     (0≦t≦1)
           i=0                 i=0
    
    と表される曲線である。ここで、
         n-1Ci = (n-1)!/i!(n-i-1)!
    n-1
        Σ wi(t) = 1
    i=0
    
    である。重み wi(t)>0 は2項分布の密度関数になっており、ブレンド関数(blending function)とも呼ばれる。

     ベジエ曲線は以下の性質を持つ:

    1. 曲線の始点は P0、終点は Pn-1 である (P(0)=P0, P(n-1)=Pn-1)。曲線は始点と終点以外は必ずしも通らない。
    2. パラメータ t に関する P(t) の1次導関数を求めてみると P'(0)=(n-1)(P1-P0), P'(n-1)=(n-1)(Pn-1-Pn-2) であることより、始点と終点における接線の傾きはそれぞれ両端の2点を結ぶ線分の傾きと等しい。
    3. 重み関数の総和が0であることから、曲線が点列 P0, P1, ..., Pn-1 の凸包(これらの n 点を内部に含む最小の凸多角形)の外に出ることはない。
  2. 2次ベジエ曲線

     3点 P0, P1, P2 によって定まるベジエ曲線を2次ベジエ曲線という。上式より、次の関係が成り立つ(というか、成り立つように wi を定める):
        P(t) = w0(t)P0 + w1(t)P1 + w2(t)P2
        ただし、w0(t) = (1-t)2,  w1(t) = 2(1-t)t,  w2(t) = t2
    
    例えば、P0=(0,0), P1=(1,2), P2=(2,1) のときの ベジエ曲線 を描いてみよう(下左図)。
  3. これまでもソースコードは、多少の無駄があっても分りやすいことを念頭に書いてきた。このソースコード もそのような主旨で書いたものであるが、プログラミングへの入門であることも考慮して、同じ結果を導くとしてもいろんなやり方があることを示すこともしばしばやってきた。 そういう意味で このソースコード とは違う書き方をしたものが これ である。 後者では、メソッドがなすべき仕事を明瞭にするために、メソッドの中で使うデータ(パラメータ)はきちんと引数に入れてある(配列 Ax と Ay を大域的配列とせずに、メソッド P の引数にして渡している)。また、メソッドが計算する値は一対の実数であるが、これを1つの値として持ち帰るためには、例えば _xyz クラス(ここ を参照のこと)のように複数の値の組を値として持つことのできるクラスを使うのも手である(ただし、これ ではメソッドを2つ用意した)が、メソッドへ渡す前に組を構成する値を設定しなければならないし、メソッドから戻ってきた値の組も分解して使わなければならない。それに対し、ここ で行なっている配列 C を使う方法は、シンプルで実行時間(パラメータの受け渡しに要するオーバーヘッド)も(僅かではあるが)メモリ使用料も少なくて済む。配列は参照型なので、引数受け渡し前後に値のコピーをする必要がないからである。

  4. P1 と P3 の中点が P2 となるような場合、2つのベジエ曲線 (P0, P1, P2) と (P2, P3, P4) を P2 で接続すると 上右図 のような曲線 (P0, P1, P2, P3, P4) が得られる。ここで、P3=(3,0), P4=(4,3) で、P2 は P1, P3 の中点になっている。
     点列をうまく選べば、この方法を拡張してもっと多数の点を制御点とするベジエ曲線を描くことができる。
    さらに、次に述べる3次のベジエ曲線を2つ使えば、制御点が7つの3次ベジエ曲線を描くことができる。

  5. 3次ベジエ曲線

     4点 P0, P1, P2, P3 によって定まるベジエ曲線を3次ベジエ曲線という。上式より、次の関係が成り立つ:
        P(t) = w0(t)P0 + w1(t)P1 + w2(t)P2 + w3(t)P3
        ただし、w0(t) = (1-t)3,  w1(t) = 3(1-t)2t,  w2(t) = 3(1-t)t2, w3(t) = t3 
    

     上記の4点 P0, P1, P2, P3 を制御点とする3次ベジエ曲線を描いてみると下図のようになる。2次ベジエ曲線と比較してみよ。始点と終点以外の点は通っていないことに注意する。

  6. 同様に、任意個数の点を制御点とするベジエ曲線が定義できるが、データ変更への対処のしやすさ等を考慮して、通常は3次までのベジエ曲線が使われているようである。

  7. スプライン曲線

     有限個の点列が与えられたとき、それらを通る滑らかな曲線としてスプライン曲線 (spline fit curve.spline は「たわみ」という意味である) と呼ばれるものがCAD等の形状設計において広く使われている(この曲線を表す関数をスプライン関数という)。

     n 個の点列 P0(x0, y0), P1(x1, y1), ..., Pn-1(xn-1, yn-1) が与えられたとき、次の条件を満たす関数 f(x) をスプライン関数(スプライン補間関数)という:

  8. このような関数 f(x) を具体的に求めてみよう。ここでは小区間 [xi, xi+1] を補間する関数 fi(x) として3次関数を用いる(このように3次関数を使ってスプライン関数を定める方法を3次スプライン補間という)。

      Fi(t) = ait3 + bit2 + cit + di  (0≦t≦1)  ・・・ (*)

    とする。Fi(t), 0≦t≦1, は fi(x), xi≦x≦xi+1, を t を使ってパラメータ表示したものである。 以後、fi(xi) を zi と表すことにする。
    Fi(0) = zi, Fi(1) = zi+1 であるから、

      di = zi
      ait3 + bit2 + cit + di = zi+1


    である。Fi(t) の t に関する導関数 Fi'(t) は

      Fi'(t) = 3ait2 + 2bit + ci

    であり、Fi'(0) = fi'(xi), Fi'(1) = fi+1'(xi+1) であるから、

      ci = zi'
      3ait2 + 2bit + ci = zi+1'


    である。ここで、fi'(xi) を zi' で表した。
    以上より、(*)の係数は次のように定まる:

      ai = 2(zi - zi+1) + (zi' + zi+1')
      bi = 3(zi+1 - zi) - (2zi' + zi+1')    ・・・ (**)
      ci = zi'
      di = zi


    一方、2次導関数 Fi"(t) は

      Fi"(t) = 6ait + 2bi

    であり、上記の「点 Pi において2次導関数も連続である」という条件より、Fi-1"(1) = Fi"(0) でなければならない。よって、

      6ai-1 + 2bi-1 = 2bi (1≦i≦n-2)

    である。これと(**)より、次の関係式が得られる:

      zi-1' + 4zi' + zi+1' = 3(zi+1 - zi-1) (1≦i≦n-2)  ・・・ (***)

    (***)の関係式が n-2 個しかないのは、f の始点 f0(x0) と終点 fn-1(xn-1) における境界条件が設定されていないからである。境界条件の設定の仕方はいろいろあるが、ここでは「両端点における2次導関数の値が等しい」というごく自然な条件(この条件は端点の曲率を0にすることに等しいので、端点に曲げを加えないということに相当する)を用いることにする。すなわち、

      F0"(0) = 0, Fn-1"(0) = Fn-2"(1) = 0

    とする。よって、

      2z0' + z1' = 3(z1 - z0)
      zn-2' + 2zn-1' = 3(zn-1 zn-2)


    であり、これと(***)を合わせた n 個の関係式は次のように行列で表すことができる(Aは@を一般的に書き直したものである):
    zi = fi(xi) = Fi(0) は既知である(与えられる)から、これは zi' = fi'(xi') = Fi'(0) を未知数とする n 元連立1次方程式である。 Aを n 個の1次方程式として書くと次のようになる:
    これを解きたい。

  9. 未知数の個数が多い連立1次方程式の解法の代表的なものはガウスの消去法掃き出し法ともいう)とLU分解法である。

     掃き出し法は、1行目から順に、Aの係数行列の対角成分(d1〜dn)が1になるように加減乗除していき、その過程の最後で xn の値が確定するので、そのあとは逆に下の行から1行目に向って xn-1〜x1 の値を順次確定していくという方法である。 この方法をAに適用した場合、解は次のように求められる。

    1. 1行目の式を d1 で割る。すなわち、
      b1' ← b1/d1
      c1' ← c1/d1
    2. 書き換えられた1行目の式を a2 倍して2行目の式から引く。すなわち、
      d2' ← d2 - a2b2'
      c2' ← c2 - a2c1'
    3. 書き換えられた2行目の式を書き換えられた d2 で割る。すなわち、
      b2' ← b2/d2'
      c2' ← c2'/d2'
    4. 書き換えられた2行目の式を a3 倍して3行目の式から引く。すなわち、
      d3' ← d3 - a3b2'
      c3' ← c3 - a3c2'
    5. 以下同様に繰り返すと、最後に
      dn'xn = cn'
      が得られる。
    6. 今度は逆に、下の行から順に xi を求めていく。まず、
      xn = cn'/dn'
    7. xn-1 = cn-1' - bn-1'xn
      xn-2 = cn-2' - bn-2'xn-1
      ・・・
      x2 = c2' - b2'x3
      x1 = c1' - b1'x2

  10. 一方、LU分解法は、連立方程式A Ax = c の係数行列 A を下三角行列 L と上三角行列 U に分解(LU 分解という)して、

      L(Ux) = c

    を解くという方法である。まず、Ly = c となる y を求め、次いで Ux = y となる x を求める。L, U
    とおいて、それらの積 LU = A の成分比較を行なうなどして、まず αi, βi, γi を決め、次いで yi を求め、最後に xi を求めると、次のような漸化式が得られる:

      γ1 = d1, α1 = b11
      βi = ai, γi = di - αi-1βi, αi = bii  (2≦i≦n)
      βn = an, γn = dn - αn-1βn
      y1 = c11, yi = (ci - βiyi-1)/γi  (i = 2, 3, ..., n)
      xn = yn, xi = yi - αiyi+1 (i = n-1, n-2, ..., 2, 1: i が大きい方から順に計算する)


    βi = ai (1≦i≦n) なので βi を ai に置き換えると、

      α1 = b1/d1
      γi = di - αi-1ai, αi = bii  (2≦i≦n-1)
      γn = dn - αn-1an
      y1 = c11, yi = (ci - aiyi-1)/γi  (i = 2, 3, ..., n)
      xn = yn, xi = yi - αiyi+1 (i = n-1, n-2, ..., 2, 1: i が大きい方から順に計算する)


    となり、di (1≦i≦n) は2,3行目で1回使われるとその後使われないので、γi の値を di に保持することにすると、

      α1 = b1/d1
      di ← di - αi-1ai, αi = bi/di  (2≦i≦n-1)
      dn ← dn - αn-1an
      y1 = c1/d1, yi = (ci - aiyi-1)/di  (i = 2, 3, ..., n)
      xn = yn, xi = yi - αiyi+1 (i = n-1, n-2, ..., 2, 1: i が大きい方から順に計算する)


    となる。また、良く見ると、αi (1≦i≦n-1) の値を bi に保持することにしても問題が生じないことが分かるので、次のように計算を進めても良い:

      b1 ← b1/d1
      di ← di - bi-1ai, bi ← bi/di  (2≦i≦n-1)
      dn ← dn - bn-1an
      y1 = c1/d1, yi = (ci - aiyi-1)/di  (i = 2, 3, ..., n)
      xn = yn, xi = yi - biyi+1 (i = n-1, n-2, ..., 2, 1: i が大きい方から順に計算する)


  11. 下左図の点列を補完するスプライン曲線を描いたものが下右図である。
    1. ソースコード (連立1次方程式の解法は LU 分解法に基づいている)
       
    2. 連立1次方程式の解 xi (1≦i≦n) が求まれば、まず zi' = xi-1 (0≦i≦n-1) が決まる。
    3. zi' = fi'(xi) = Fi'(0) = ci であることから、直ちに ci が定まる。
    4. di = zi = fi(xi) = Fi(0) は与えられているので、(**)より ai, bi も定まる。以上で、Fi(t) = ait3 + bit2 + cit + di が決定される。
       
    5. 演習問題: 連立1次方程式を掃き出し法(ガウスの消去法)で解いてみよ。
     ⇒