軸方向に単調なBezier曲線の最小二乗法
最小二乗法を用いて, 指定点を通るようなBezier曲線を求めたい. はじめに, Bezier曲線のパラメータの関係で, 例えばx, y平面を考える場合は, xまたはyについて単調増加(あるいは単調減少)である必要がある. 最後にPythonによって実装を書いておいた.
間違っている可能性があるので注意.
[目次]
Bezier曲線の定義
空間上に点があるとする. 制御点とパラメータによって, 次元Bezier曲線は次のように定義される;
ここには次式で定義されるBernstein関数である;
以降は点と制御点とをまとめて制御点と呼ぶことにする(の計算から, その方が便利なことがわかる). なおをに代入してみれば明らかなように, Bezier曲線は必ず点を通る.
Bezier曲線による最小二乗法
空間上にBezier曲線が通ることを期待したい点があり, 点各々に対してBezier曲線のパラメータがわかっているとする. このとき, 点を通るような次元Bezier曲線の制御点を見つけたい. ただし上側添字は転置を意味する. 最小二乗法を考えれば, 点について
を最小にするような制御点を見つけることになる. ただし, である. 今の問題では制御点がパラメータのようなものなので, (1)で与えられるBezier曲線をの関数とした.
ここで問題は, 仮定から明らかなように, 点とパラメータがあらかじめわかっていなければならないことである. 点だけが与えられ, 点を通るBezier曲線を求めたい場合, がわかっている場合は少ない(と筆者は思う)ので, この方法は(筆者にとっては)微妙である......(の場合についてはわかっているがそれ以外がわからない.)
そこで, 点のパラメータのいずれか1つの単調性を仮定する. 単調変化するパラメータの座標値を0から1の範囲に規格化することでパラメータを代用することができる. 例えばパラメータxが単調変化する場合は, 規格化することで, となる!
以下では簡単のために, 平面で点のパラメータについて単調性を仮定する. パラメータをBezier曲線のパラメータとみなすために, 先に点の座標の最大値および最小値を用いて, Min-Max Normalizationを行っておく;
ただしこの方法ではが、がに対応する*1. そこで座標系の最大値と最小値を用いても良い;座標がの範囲だと思って式(4)を使えば良い.
これによってパラメータを代用できる(Bezier曲線を求めたら, 上の計算をについて計算すれば, 元の座標系に戻すことができる). また1つの軸をパラメータに見立てることで, 制御点はベクトルで計算する必要がなくなり, 制御値(座標)を求めるだけでよくなるため, 式(3)は次のようになる;
ただし, である. 式(5)を制御値で微分する. はスカラーの一次関数なので瞬時に計算できて
について足し合わせる. これが0となるようなを探していることを思い出す;
展開して制御値について整理すれば,
頑張って整理すると次のようになる;
この方程式を解けば, 制御値を求めることができる(逆行列を作用させるだけで良い).
Pythonで実装
これをPythonで実装した. 環境は下の通り.
Python==3.8.3 numpy==1.19.1 scipy==1.5.2 matplotlib==3.3.1
from scipy.special import comb import numpy as np import matplotlib.pyplot as plt def bernstein(n, i, t): return comb(n, i) * t**i * (1 - t)**(n-i) def bezier(n, t, P): return np.dot([bernstein(n, i, t) for i in range(n + 1)], P) if __name__ == '__main__': # 通りたい点Q Q = np.array([(0, 0), (1, 2), (3, 0), (4, 4)]) x = Q[:, 0] y = Q[:, 1] # 今回はQのx座標が単調増加しているので, # xをパラメータtに変換する. x_max = x.max() x_min = x.min() t = (x - x_min) / (x_max - x_min) # Bezier曲線の次元. n = 3 # 式(10)の方程式を解く. # Qによっては逆行列が存在しないので, # np.linalg.solve(方程式)やnp.linalg.inv(逆行列)では計算できない. # そこでnp.linalg.pinv(模擬逆行列)で強引に計算している. A = np.array([[bernstein(n, i, tj) for i in range(n+1)] for tj in t]) P = np.dot(np.linalg.pinv(A), y) # 制御値 # 求めたBezier曲線 x_lsm = np.linspace(0, 1, 100) * (x_max - x_min) + x_min y_lsm = np.array([bezier(n, t, P) for t in np.linspace(0, 1, 100)]) plt.figure() plt.plot(x_lsm, y_lsm, label="estimated") # Bezier曲線 plt.plot(x, y, '--x', label="Q") # 入力点 plt.plot(x, P, '--o', label="P") # 制御値 plt.legend() plt.show()
初めてをブログ記事内でガッツリ書いた. はそこまで使い慣れているわけではなく, いまだにレイアウトがよくわかっていない......
*1:しかし実はBezier曲線がとを通るとは限らない......最小二乗法なので仕方ないと思っているけど数学的にはどうなのかな.