Atom Graphで画像処理をしてみる

前回紹介したnPass Element GraphはSubstance 3D Designerで言うところのFX-Mapのようなものでした。
個人的にはFX-Mapよりわかりやすくて好きです。

さて、SDにおける特殊なノードといえば、FX-MapとPixel Processorではないかと思います。
今回はInstaMATの中でPixel Processorに相当するAtom Graphを紹介します。
以前、SDでも実装したSymmetric Nearest NeighborをInstaMATでも実装して、実際の使い方を見ていきます。

まずはグラフ全体を見てみます。

さすがにグラフすべてを表示・解説できないので、作成したものをGitHub上にアップしています。

https://github.com/Monsho/InstaMATLibrary/blob/main/graphs/SymmetricNearestNeighbor.IMP

では、最初から作っていきます。

Atom Graphを新規作成し、テンプレートとしてDefaultを選択します。
すると以下のようなグラフが作られます。

Element Graphはノードごとに出力があり、その出力を繋げていくことで最終的な結果を得るものです。
SDや、UEのマテリアルなんかもこの手法です。
この手法は単純でわかりやすい部分はあるものの、特にループのようなフロー制御を実現するのが難しいという問題があります。

これに対してAtom GraphはUEのBlueprintに近い思想のツールと言えます。
Element Graphとの大きな違いは実行コネクタが有ることです。Blueprintにもありますよね?それと同じです。
上の画像にある[Entry]ノードが処理の開始、[Return]ノードが処理の終了です。
それらに存在する白のコネクタが実行コネクタで、[Entry]から繋がっていく順番で処理されていきます。

また、Atom Graphでは出力として[AtomOutputImage]と[AtomOutputImageGray]のどちらかしか選択できません。
つまり、カラーかグレースケールのイメージとしてのみ出力できます。
しかしAtom Graphで処理するのはカラーそのものであってイメージではありません。
そのため、[Write Pixel]ノードを使って処理中のピクセルに対しての書き込みを行います。
そしてその書き込み結果となるイメージを出力として渡すことになります。

上の図はそれらの一連の流れをデフォルトとして用意しています。
ユーザーはこの[Write Pixel]に入力するカラー情報を計算します。

次に入力イメージを設定し、そのイメージからテクセルを取得する方法についてです。
まず、グラフの入力として[AtomInputImage]を追加します。名前は適当でいいですが、今回は[Input]と名付けました。
そしてテクセルをサンプリングする命令をグラフに追加します。これには[Read Pixel]ノードを利用します。
このノードにはフィルタリング方法などでいくつかの手法が用意されていますが、今回追加するのは[Read Pixel (Nearest)]です。

入力としてはイメージ、テクセル座標、UV方向のリピート設定が用意されています。
リピートはどちらも[Clamp]にして繰り返しをしないようにします。
イメージにはグラフ入力として追加した変数を接続します。
テクセル座標は[Get Texel Coordinate]ノードを利用します。
このノードでは処理中のテクセル座標を取得できますが、取得される値は0.0~1.0のUV値です。[Read Pixel]に設定する座標も同様です。

最終的に[Read Pixel]の出力を[Write Pixel]に入力すると画像をそのまま出力するだけのものとなります。
グラフは以下のようになります。

実行コネクタも接続しましょう。
出力画像の結果を見たい場合、[Set Output]ノードを右クリックし、[画像'Output'を表示]を選択すると2Dペインに表示されます。
Element Graphと違い、Vキーのショートカットは対応していません。

例えば、取得したテクセル座標にVec2(0.5, 0.0)を加算してみましょう。

出力結果は中心から半分ズレた状態になります。ここからテクセル座標が0.0~1.0であることがわかりますね。
そのため、1ピクセルをずらしてサンプリングする場合、"1 / テクスチャサイズ"をずらす必要があります。
この1ピクセル分のテクセル座標を求めるには2つの方法があります。

1つはイメージから[Get Texel Size]を取得する方法です。
このノードは1テクセルのサイズであり、前述の計算で求められる値が得られます。

もう1つは[Get Image Size Vec2]でイメージサイズを取得し、[Vec2/Vec2]でコネクタ[A]を1.0にする方法です。
どちらの方法でも結果は同じなので好きな方を選んでください。

では、本題となるループについてです。
ループは以下のように利用します。

[Loop]ノードは実行コネクタの入出力と別に、[Condition]という入力と[LoopBody]という出力があります。
[Condition]はループを実行する条件で、条件がTrueの場合にループが継続します。
[LoopBody]はループ中に実行する処理です。

ループ回数の制御はローカル変数を利用します。
ローカル変数にint32の[X]を追加し、[LoopBody]の処理内で加算していきます。
[Condition]には[X]の比較処理を行います。上の画像では0~9までの10回ループです。
[X]の値は[Loop]の前で初期化しておきます。
ローカル変数の項目で初期化されていますが、変数を使いまわした場合は汚れているので、初期化のクセをつけておくことをおすすめします。

上の画像では[LoopBody]の中で[Read Pixel]の結果を[TotalColor]というローカル変数に加算しています。
10回ループなので、同じテクセルの値を10回加算しているだけということになります。
この結果は[Loop]の先の[Write Pixel]に入力します。
結果として、入力画像のカラーを10倍した結果が出力されます。

最後に、[LoopBody]の処理の終点として[Continue]ノードを利用しています。
C++などのループを知っていればわかるかと思いますが、[Continue]は1回のループ処理をそこで終了し、次のループ処理を行うフロー制御命令です。
プログラミング言語ではループブロックが明確になっている場合が多く、ブロックの終点処理は特に何もしなくても次のループに進みます。
しかし、InstaMATでは明確な終点ノードを設定する必要があります。
[Continue]は基本的なループ終点処理となります。
他にも、ループ自体を終了する[Break]、グラフ自体を終了する[Return]も選択可能です。

ここまでくればある程度のことが可能になります。
SNNフィルタは少し面倒なので、簡単なボックスブラーを実装して2重ループの使い方を見てみましょう。
まずは[Entry]から多重ループの部分。

外のループは[X]、中のループは[Y]をループカウントとします。
[Kernel]は入力パラメータで、ボックスブラーのカーネルサイズを指定します。
デフォルトの3を指定すると、-3~3の間でループするので、7x7のボックスブラーとなります。
外ループの前で[X]を初期化、中ループの前で[Y]を初期化します。

次に中ループの[LoopBody]処理です。

ローカル変数[X]と[Y]分だけずらした座標からテクセルを取得し、[TotalColor]に加算していきます。
前述したように、[Get Texel Size]で1ピクセル分のUV座標オフセットを取得できますので、[X], [Y]をそれぞれの要素に乗算すれば必要なオフセット値を得られます。
色の加算後に[Y]の値をインクリメントし、[Continue]で終点とします。 また、中ループの実行コネクタの先では[X]の値をインクリメントと[Continue]も必要です。

最後に[TotalColor]をサンプリングした回数で除算すればボックスブラーの結果を得られます。

[Kernel]の値を増やせば出力画像のぼやけが強くなるのがわかるはずです。
SNNフィルタはこれの応用のようなものなので、ループ部分はちょっと複雑に見えますが似たようなものです。

1つだけ解説しておくとすると、SNNでは中心ピクセルと対称ピクセルの色の距離を求める必要があります。
これをAtom Graph内で対応するのはちょっと面倒なのでFunction Graphを利用しています。
[ColorDistance]というグラフを作成し、中心カラーと対称カラーの2つの色を入力し、色の距離が近い方を選択するようにしています。
選択には[Branch]ノードを利用していますので、簡単な分岐のサンプルとして見てもいいと思います。
なお、色の距離としては輝度計算を行って、輝度が近いものを選んでいます。

[Test]グラフでは環境マップの画像に適用した結果を見ることができます。

絵画調っぽくなっていますね。
なお、環境マップはデフォルトでは線形となっていて、SNNフィルタに設定すると"色空間が違います"というような警告が出ます。
この場合、環境マップをsRGBに変更することで警告なく、色も元画像と同じようになります。

また、グラフノードはグラフ自体のサイズを継承することしかできず、入力イメージのサイズを継承できません。
この場合、ノードのパラメータで[要素の形式]内にある[絶対サイズ]を有効にしてサイズを指定することで対応できます。
もしくはグラフ自体のサイズを画像サイズに合わせましょう。

と、このようにInstaMATでは比較的わかりやすくループが利用可能です。
SDはループを考慮した初期実装になっていなかったため、ちょっとわかりにくい実装になってしまっているという印象です。
この辺り、後発ソフトの強みが出ているなと感じます。

ただし、注意が必要です。
SDのループには最大ループ回数が設定されていて、無限ループにならないような工夫がされていました。
これに対してInstaMATでは特にそのような機能が存在しません。
ただ、1重ループの場合は故意に無限ループにしても結果が正常にならないだけで停止したりはしませんでした。 しかし、2重ループで両方無限ループにするとOSを巻き込んでフリーズしました。
PCリセット案件となりますので注意しましょう。