画像処理によるメタボール表現(2020.05.17)

メタボールについて

 この日の作品は、画像処理によるメタボール表現を行うものでした:

メタボールとは…と説明をしだすと、ちょっと複雑になりますが、 ひとつのメタボールの存在は空間上に影響を与え、 その影響は関数 \( f_i \) にて測定することができるーとします。

ここで \( f_i \) の添字 i は i 番目のメタボールという意味として理解して下さい。

で、この関数の引数は空間中のある位置 p とします。 つまり、\( f_i(p) \) で、空間中のある点 p の、 メタボール i による影響が測定できる、と考えます。

空間中におけるメタボールの影響は加算的であると考えます。 つまり、N 個のメタボールが存在するとき、 点 p におけるメタボールが与える影響の大きさは \( \sum_{i=1}^N f_i(p) \) にて測定できる、とします。

これをここでは関数 f と表すことにします。

 メタボールによる形状は、この関数 f を用いて、 \( f(p)=c \) を満足する点 p の集合、と定義されます (c はとある定数を表すとします)。

厳密に言えば、このように定義された形状は 一般的には表面のみを表しますので、 形状内部までを表すのであれば、 \( f(p) \le c \) を満たす p の集合、と表されます。

素直な実装方法

 1 つのメタボールが空間に与える影響は、 空間における電荷などを模したものを使うのが一般的です (メタボール=等電位面のアナロジー)。

しかし、ここでは単に半径 \( r_i \) のメタボールの中心位置 \( p_i \) とし、 メタボールの中心では、\( f_i \) は \( r_i \) を返し、 円周上では 0 となる関数を考えてみます。

$$ \left\{ \begin{array}{l} f_i(p_i)=r_i \newline f_i(p)=0 \hspace{1ex} ( \mbox{ただし $p$ は 円周上の点とする} ) \end{array} \right. $$

メタボールの中心と円周上以外では、中心からの距離が半径より小さい場合は r-d を、半径より大きな場合は 0 とすることにしましょう (変数 d は、空間中の任意の点 p とメタボールの中心点との距離を表しています)。

少々天下り的ですが、これをまとめると、

$$ f_i(p)=\max \left( r- \left| p-p_i \right|, 0 \right) $$

と記述できます。

 メタボールは任意の次元にて定義可能です。 ここでは 2 次元空間の場合について考えてみると、 \( f_i \) は、

$$ f_i(x,y)=\max \left( r- \sqrt{ \left(x-x_i\right)^2+\left(y-y_i\right)^2 }, 0 \right) $$

と書き表されます。

N 個のメタボールの場合は、当然

$$ f(x,y)=\sum_{i=i}^N f_i(x,y)=\max \left( r- \sqrt{ \left(x-x_i\right)^2+\left(y-y_i\right)^2 }, 0 \right) $$

となります。

 メタボールの内部を例えば白色で塗りつぶす場合、 全てのピクセルの (x,y) に対して上記の関数 f を計算し、 例えばその値がある値 c より大きかったら、そのピクセルを白色で塗る、 という処理となります。

プログラムで書くと次のようになります:

def setup():
    size(500,500)

N=10
P=[ [random(500),random(500),random(100)] for i in range(N) ]

def f(x,y):
    result=0
    for i in range(N):
        xi=P[i][0]
        yi=P[i][1]
        ri=P[i][2]
        result+=max(ri-dist(x,y,xi,yi),0)
    return result

def draw():
    clear()
    stroke(255)
    c=5
    for y in range(500):
        for x in range(500):
            if f(x,y)>c: point(x,y)

画層処理を用いた方法

 素直な実装では、全てのピクセルに対して、f の値を計算する必要がありました。 しかし、関数 f の定義をよく見ると、それぞれのピクセルに対し、 関数 \( f_i \) の値を累積していっても f の値を得ることができそうです。

何を言っているのかというと、 あるピクセルの位置 (x,y) を固定し、\( f_i \) を計算するのではなく、 \( f_i \) の方、添字 i の方を固定し、 全ての (x,y) について累積的に値を計算していっても問題ない、 ということです。

実際のところ、\( f_i \) が 0 となるピクセルについては、 計算しなくても関数 f の値は変わりません。

 今、\( f_i \) の定義により、\( f_i \) が 0 でない領域は、 点 ( \( x_i, y_i \) ) を中心とする半径 \( r_i \) の円の内部でした。

であれば、加色混合モードで、色を変化させながら同心円として 円領域を描画すれば、全てのピクセルに f の値が蓄積されそうです。

 もちろん、ピクセルに格納されるのは任意の浮動小数点値ではなく、 Processing の場合は R,G,B 各チャネル 8 ビットの整数のようです。

なので、関数 f の値としてピクセルに格納される値の精度は 低下してしまいますが、それでもやってみる価値はありそうです (というか、実際にそれで冒頭に紹介した作品は成立しているので、 ある程度であれば問題なくメタボールの表現は可能です)。

 実際のコードは以下のようになります:

def setup():
    size(500,500)
    noFill()
    strokeWeight(2) # 注1
    blendMode(ADD)  # 加色混合モードを指定

def C(x,y,r):
    for t in range(r):
        stroke(t)   # 色として fi の値を指定
        circle(x,y,r-t)

N=lambda t:noise(sin(t),frameCount*.01)*500

def draw():
    clear()
    for i in range(10):
        C(N(i),N(i*.3),i*15+5)
    filter(THRESHOLD,.2)

このプログラムでは、関数 C により、 点 (x,y) を中心とする半径 r の円を、 \( f_i \) の値を用いて塗りつぶします。

関数 C で描かれる円領域は、 円周の集合体として描かれますので、 半径を 1 づつ変化させる状況では、 隙間ができてしまう可能性があります。

そのため、注 1 のところで指定しているように、 strokeWeight(2) として、円周の幅を少し太くしています。

 draw 関数にある for ループを抜けた時には、 全てのピクセルに関数 f の値が格納されています。

その値を参考に、ある値以上のピクセルを可視化するため、 filter(THRESHOLD,0.2) を実行しています。

実際には、255*0.2=51 ですので、 この作品では f(x,y)>51 以上の領域を描き出していることとなります。