Python

Pythonで点群を球面近似する 最小二乗法による計算

整形外科であつかう骨には球の形状が多く見られます。

  • 肩関節の上腕骨頭
  • 股関節の大腿骨頭

などは代表的な球の形状をしている骨です。

計測や形状評価をするにあたって、これらの球の中心や半径を求めることがあります。
ここではその方法を記載します。

計算原理は式の変換がやや難しめなので、

「球面近似のPythonスクリプト」

からでもわかるようにしてあります。
 
=>球近似のPythonスクリプトはこちら

球面近似計算の原理

点群から球の半径と中心を近似する方法は

「最小二乗法」

で求めることができます。

半径を\(r\)とし、中心位置のベクトルを\(m\)とします。 そして点群のベクトルを\(v_k\)とします。

ここで

\(C = \displaystyle \sum_{k=1}^{n}\left\lbrace (v_k – m)^2 – r^2 \right\rbrace ^2 \)

を最小とする\(r\)と\(m\)を考えます。

\(r\)と\(m\)に対して偏微分をし、0になる値を求めます。

\(r\)に対して偏微分をすると、

\(-4r \displaystyle \sum_{k=1}^{n}\left\lbrace (v_k – m)^2 – r^2 \right\rbrace = 0 \)

半径\(r\)は0ではないとして、

\(r = \sqrt{\displaystyle \frac{1}{N}\sum_{k=1}^{n}(v_k – m)^2}\) ・・・式①

となります。

\(m\)による偏微分は

\(\displaystyle \sum_{k=1}^{n}(v_k – m)\left\lbrace (v_k – m)^2 – r^2 \right\rbrace = 0\)

ここに上のrを代入して展開して整理をすると

\(\displaystyle \frac{1}{N}\sum_{k=1}^{n}v_k^3 – \displaystyle\frac{1}{N}\sum_{k=1}^{n}v_k \cdot \displaystyle\frac{1}{N}\sum_{k=1}^{n}v_k^2 – 2 \left\lbrace \displaystyle \frac{1}{N}\sum_{k=1}^{n} v_k(v_k \cdot m) – \displaystyle\frac{1}{N}\sum_{k=1}^{n}v_k (m \cdot \displaystyle\frac{1}{N}\sum_{k=1}^{n}v_k)\right\rbrace \) = 0

乗数平均でこみいっているので、
3乗平均、2乗平均、1乗平均を書き換えるとスッキリします。

\(\displaystyle \frac{1}{N}\sum_{k=1}^{n}v_k^3 = \overline{v_k^3}\)

\(\displaystyle \frac{1}{N}\sum_{k=1}^{n}v_k^2 = \overline{v_k^2}\)

\(\displaystyle \frac{1}{N}\sum_{k=1}^{n}v_k = \overline{v_k}\)

とすると

\(\overline{v_k^3} – \overline{v_k}(\overline{v_k^2}) = 2\left\lbrace \displaystyle \frac{1}{N}\sum_{k=1}^{n} v_k(v_k \cdot m) – \overline{v_k} (m \cdot \overline{v_k})\right\rbrace \)

ここで

\(a (b \cdot c) = (a \cdot b^{\mathrm{T}}) \cdot c \)

※ ベクトルは縦行列なので
\( (a \cdot b^{\mathrm{T}})\) は3×3行列

例えば

\(
a =
\left(
\begin{array}{c}
a_1 \\
a_2 \\
a_3
\end{array}
\right)
\) \(
b =
\left(
\begin{array}{c}
b_1 \\
b_2 \\
b_3
\end{array}
\right)
\)

であれば

\(a \cdot b^{\mathrm{T}} =
\left(
\begin{array}{ccc}
a_1b_1 & a_1b_2 & a_1b_3\\
a_2b_1 & a_2b_2 & a_2b_3 \\
a_3b_1 & a_3b_2 & a_3b_3
\end{array}
\right)
\)
です。

これを利用すると

\( A = 2\left( \displaystyle \frac{1}{N}\sum_{k=1}^{n} v_k \cdot v_k^{\mathrm{T}} – \overline{v_k} \cdot \overline{v_k}^{\mathrm{T}}\right) \) ・・・式②

\(b = \overline{v_k^3} – (\overline{v_k^2})\overline{v_k}\) ・・・・・・式③

として

\(A \cdot m = b\)

と表記できます。

ここから

\(m = A^{-1} \cdot b\)

以上で

半径 \(r\)
中心 \(m\)

が求まります。

ベクトルでの偏微分など、細かい点は多いですが、原理の概要がわかって実用できればいいと思います。

球近似のPythonスクリプト

点群ベクトル\(v\)に対して

半径 \(r\)を

\(r = \sqrt{\displaystyle \frac{1}{N}\sum_{k=1}^{n}(v_k – m)^2}\) ・・・式①
より求め、

\(\displaystyle \frac{1}{N}\sum_{k=1}^{n}v_k^3 = \overline{v_k^3}\)

\(\displaystyle \frac{1}{N}\sum_{k=1}^{n}v_k^2 = \overline{v_k^2}\)

\(\displaystyle \frac{1}{N}\sum_{k=1}^{n}v_k = \overline{v_k}\)

として

\( A = 2\left( \displaystyle \frac{1}{N}\sum_{k=1}^{n} v_k \cdot v_k^{\mathrm{T}} – \overline{v_k} \cdot \overline{v_k}^{\mathrm{T}}\right) \) ・・・式②

\(b = \overline{v_k^3} – (\overline{v_k^2})\overline{v_k}\) ・・・・・・式③

\(A \cdot m = b\)
より
中心 \(m\)を求めます

計算をスクリプトに書いていきます。

ファイル名は
「fitting.py」
とします。

fitting.py


def sphere_fit(point_cloud):
    """
    入力
        point_cloud: 点群のxyz座標のリスト numpyのarray形式で3x3xN行列
    出力
        radius : 近似球の半径 スカラー
        sphere_center : 球の中心座標 xyz numpyのarray
    """

    A_1 = np.zeros((3,3))
    #Aのカッコの中の1項目用に変数A_1をおく
    v_1 = np.array([0.0,0.0,0.0])
    v_2 = 0.0
    v_3 = np.array([0.0,0.0,0.0])
    #ベクトルの1乗、2乗、3乗平均の変数をv_1, v_2, v_3とする
    #1乗、3乗はベクトル、2乗はスカラーとなる

    N = len(point_cloud)
    #Nは点群の数
    
    """シグマの計算"""
    for v in point_cloud:
        v_1 += v
        v_2 += np.dot(v, v)
        v_3 += np.dot(v, v) * v
        
        A_1 += np.dot(np.array([v]).T, np.array([v]))
        
    v_1 /= N
    v_2 /= N
    v_3 /= N
    A = 2 * (A_1 / N - np.dot(np.array([v_1]).T, np.array([v_1])))
    # 式②
    b = v_3 - v_2 * v_1
    # 式③
    sphere_center = np.dot(np.linalg.inv(A), b)
    # 式①
    radius = (sum(np.linalg.norm(np.array(point_cloud) - sphere_center, axis=1))
              /len(point_cloud))
    
    return(radius, sphere_center)
    

スクリプトでの計算例(上腕骨)

上腕骨のstl表面データを使用して、上腕骨頭の球の近似をしてみます。

Meshlabで頭の丸い部分だけ抽出し、
sphere.stl
として保存します。

=>Meshlabで表面データの一部を抽出する方法

sphere.stl

この部分の球近似を
外部でpythonインタープリタで試してみます。



import numpy as np
import fitting

from stl import mesh
# numpy-stlを使用します。

sphere_stl = mesh.Mesh.from_file('sphere.stl')
#stlファイルの読み込み
sphere_points = sphere_stl.points.reshape([-1, 3])
#stlから点群を抽出
print(sphere_points)

# [[ 154.80566  -124.725586  192.21085 ]
# [ 153.9873   -124.725586  192.33838 ]
# [ 153.9873   -124.098175  193.5     ]
# ...
# [ 157.29794  -124.725586  235.5     ]
# [ 157.26074  -124.711945  235.5     ]
# [ 157.26074  -124.725586  235.52    ]]
# 表面データの点群が代入されている

radius, center = fitting.sphere_fit(sphere_points)
# fitting.pyのsphere_fit関数で半径、中心を計算
print(radius)
# 23.087794980492543
print(center)
# [ 154.15674832 -133.40106545  213.64071843]

半径と中心が計算できました。

この球を先ほどの表面データと一緒に描画して結果を確認していきます。

半径と中心を指定して描画する

近似した球を確認する
fitting.pyに以下のスクリプトを追加して、計算結果の球を書き出してMeshlabで確認します。

fitting.pyの続き


"""半径と中心を定めて 点群ファイルに保存する"""
def draw_sphere(radius, sphere_center):
    """
    入力
        radius:半径 スカラー
        sphere_center : 中心座標のxyz  numpyのarray
    """

    point_list = []

    """球面の点群を作成していく"""
    for i in range(360):
        i = i * np.pi / 180   #ラジアン表記にする
        for j in range(360):
            j = j * np.pi / 180  #ラジアン表記にする
            point = radius * np.array([np.sin(i) * np.cos(j),np.sin(i) * np.sin(j), np.cos(i)]) + sphere_center
            #座標点を追加していく
            point_list.append(point)
    
    point_list = np.array(point_list)
    # numpyのarray形式に変換
    np.savetxt('sphere.asc', point_list)
    # Meshlabで読める.ascファイルに保存する
    return

先程のPythonインタープリタで

fitting.draw_sphere(radius, sphere_center)
# radius, sphere_centerには先程の値が入っているものとします。

とすると
「sphere.asc」に球の点データが出力されます。
これはMeshlabでインポートして開けます。

sphere.asc

抽出した表面の部分が球に近似されたのがわかります。

球面近似できるソフトウェアはいくつかありますが、自分で計算できるようになると、入力形式と出力形式が自由に扱えるので便利です。