本記事の概要
Xilinxの統合ソフトウェア開発環境では、Vitisビジョンライブラリ(以下、ビジョンライブラリ)をインクルードすることによって、画像処理機能を高速化(アクセラレーション)することが可能です。例えば、OpenCVのフィルタ処理などを、FPGAに実装可能なロジックへと高位合成することができます。
ビジョンライブラリのインクルードの方法は以下の記事にまとめています。
本サイトでは実際にVitisビジョンライブラリのFilter2D_accel関数を例に、ラプラシアンフィルタを行う画像処理IPのCシミュレーション、高位合成、C/RTL協調シミュレーション、そして実装を進めていこうと思います。最終的にラプラシアンフィルタをZynqに実装し、空間フィルタ処理した画像をディスプレイに表示させてみようと思います。
まず、本記事では、Filter2D関数の動作をテストベンチでシミュレーションしす。シミュレーションは、C/C++で記述されたソースコードが所望の機能(ラプラシアンフィルタ)を満足するかどうかを確認するCシミュレーションです。
まず、テストベンチ関数の処理の内容を理解し、そのうえで動作を確認できればと思います。
Filter2D_accelについて
まず、今回用例に採用するFilter2D_accel関数について簡単に述べておきます。
Filter2Dは、入力画像に対し線形フィルタによる空間フィルタリング処理を行う関数で、OpenCVにも実装されています。
空間フィルタリングとは、入力画像の対応する画素値だけでなく周囲の画素も含めた特定の領域内の画素値を用いて計算する画像処理のことを言います。空間フィルタは線形フィルタと非線形フィルタに分類され、周囲の画素を用いた演算が次の式のように線形変換で与えられる空間フィルタのことを、線形フィルタといいます。
要するに、Filter2Dは、入力画像に対してカーネル行列を使った畳み込み(convolution)処理を行う関数です。そのため、ビジョンライブラリの用例は、customconv(custom convolutionの略か?)という名前でディレクトリが用意されています。
線形フィルタを用いた空間フィルタリング処理では、以下のような画像処理を施すことができます。
今回は、エッジ抽出の一つである4近傍ラプラシアンフィルタを例にしてみましょう。カーネルが
$$ K =
\begin{pmatrix}
0 & 1 & 0 \\
1 & -4 & 1 \\
0 & 1 & 0
\end{pmatrix}
$$
で与えられる横方向と縦方向の2次微分の和を出力する線形フィルタです。
テストベンチを読み込む
まずは、ビジョンライブラリからFilter2D関数を読み込みましょう。読み込む方法は以下の記事を参考にしてください。
Vitis HLSを立ち上げ、空のプロジェクトを作成します。
ビジョンライブラリにおけるL1ディレクトリの用例customconvからテストベンチと高位合成対象のソースコードをそれぞれプロジェクト内にコピーします。
処理する入力画像もプロジェクトにコピーしておきましょう。今回は、解像度640×480のディスプレイに投影するため、解像度160×120の次のビットマップ画像を用意しました。640×480の画像でもよいのですが、高位合成後のロジック規模が大きくなりすぎたので、解像度を下げています。
テストベンチ関数xf_custom_convolution_tb.cppをTest Benchに、高位合成の対象となるxf_custom_convolution_accel.cppをSourceに登録します。
次に、Project>Project Settingと順に選択し、SimulationとSynthesisの設定タブでそれぞれFlagを設定し、ビジョンライブラリとOpenCVライブラリをインクルード、そしてリンクするためのフラグを設定します。
以上で、Vitis HLSへの関数の読み込みは終了です。
テストベンチの構成
パラメータ設定
今回の解像度の画像に対しラプラシアンフィルタの処理を行うため、ヘッダーファイルとテストベンチ関数のパラメータを変更しました。
xf_config_params.h
まず、xf_config_params.hから。次のようにパラメータを設定します。
#define FILTER_HEIGHT 3
#define FILTER_WIDTH 3
/* set the optimization type */
#define NO 1 // Normal Operation
#define RO 0 // Resource Optimized
/* set the output type */
#define OUT_8U 1
#define OUT_16S 0
#define GRAY 0
#define RGBA 1
それぞれのパラメータの意味は以下の通り。
パラメータ | 説明 | 今回 |
FILTER_HIGHT FILTER_WIDTH | カーネルの要素数 | 4近傍ラプラシアンフィルタでは3×3行列とする |
Optimization Type | NO:NPPCを1にする RO:NPPCを8にする | 静止画出力でレイテンシーには余裕があるので、NPPC(1クロック当たりのピクセル数)を1とするNOを選択 参考:Matクラスについて |
Output Type | 出力画像のピクセルのTypeを選択 | 入力画像は符号なし8it分解能のビットマップ。出力も同様のビットマップとするためOUT_8Uを選択 |
Channel | GRAY:グレースケール RGBA:RGB3色 | 入力画像はカラー画像のため、RGBAを選択 |
xf_custom_convolution_config.h
xf_config_params.hを上の表のように設定すると、自動的に#if-#else-#endif構文により入力画像と出力画像を格納するMatオブジェクトのTypeや配列のデータ幅が xf_custom_convolution_config.h で設定されます。
ビジョンライブラリにおけるxf::cv::Mat型の使い方については、次の記事をご覧ください。
xf_custom_convolution_config.h で、ユーザーが変更しなくてはならないのは、HEIGHTとWIDTHとSHIFTの3つです。
このうちHEIGHTとWIDTHは、入力画像と出力画像を格納するMatオブジェクトのテンプレート引数そのものなので、特に上記の記事をご覧いただければ説明は不要でしょう。
SHIFTは、空間フィルタリングのカーネルを定義するときのビット右シフトの回数を表します。
例えば、カーネルに次の平滑化フィルタを設定する場合を考えましょう(customconvはデフォルトでこのフィルタが設定されています)。
$$ K =
\begin{pmatrix}
1/9 & 1/9 & 1/9 \\
1/9 & 1/9 & 1/9 \\
1/9 & 1/9 & 1/9
\end{pmatrix} =
\begin{pmatrix}
0.1111 & 0.1111 & 0.1111 \\
0.1111 & 0.1111 & 0.1111 \\
0.1111 & 0.1111 & 0.1111
\end{pmatrix}
$$
Filter_2D_accelでは、カーネルを表す配列のデータ型がshort int、すなわち符号付き2byte整数型です。しかし、平滑化フィルタでは小数を入力しなければ正しく計算できません。
そこで、用いるのが右シフトです。カーネルを0.1111ではなく、3640に設定し、SHIFTを15とします。そうすると、
$$ 3640>>15 = 3640 / 2^{15} \simeq 0.1111 $$
なので、小数を表現することができます。
ラプラシアンフィルタの場合、整数なのでビットシフトを設ける必要はありませんが、次のように8ビット分シフトさせて小数も代入できるようにしました。
#define HEIGHT 120
#define WIDTH 160
/* specify the shift parameter */
#define SHIFT 8
filter_ptr[0] = 0; //0;
filter_ptr[1] = 1 << SHIFT;
filter_ptr[2] = 0; //0;
filter_ptr[3] = 1 << SHIFT;
filter_ptr[4] = -4 << SHIFT;
filter_ptr[5] = 1 << SHIFT;
filter_ptr[6] = 0; //0;
filter_ptr[7] = 1 << SHIFT;
filter_ptr[8] = 0; //0;
一連の処理の流れ
次に、具体的にテストベンチの処理を確認しましょう。
テストベンチxf_custom_convolution_tb.cppのメイン関数は以下の順に処理がなされています。
OpenCVのcv::Matクラスとのデータの受け渡し
「⑤出力画像の算出」でOpenCVのcv::Matクラスのオブジェクトimg_inから、ビジョンライブラリのxf::cv::MatクラスのオブジェクトimgInputにデータを移すための受け渡し方法をまとめておきます。
Filter2D_accel関数を呼び出すとき、OpenCVのcv::Matクラスのオブジェクトを引数にとれず、配列しか渡すことができません。そこで、テストベンチ関数と高位合成対象の関数では、次のようにして画像を渡しています。
高位合成を行う対象のソースファイル内では、OpenCVのライブラリを参照することはできません。ビジョンライブラリの関数でないと高位合成できないためです。
そのため、テストベンチでは、cv::Matのオブジェクトにおけるdata配列(のポインタ)を高位合成する対象の関数に渡して、関数xf::cv::Array2xfMatを用いて配列をxf::cv::Matのオブジェクトへと変換します。
xf::cv::Matクラスのオブジェクトへと変換したら、空間フィルタリングを行う関数xf::cv::filter2Dに代入します。
高位合成対象の関数内で所望の機能の処理を行ったのち、関数 xf::cv::xfMat2ArrayでMatオブジェクトを配列に変換してから、テストベンチ関数に戻ります。
Cシミュレーションの結果
では、実際にCシミュレーションを実行してみましょう。
実行の結果は、無事誤差なく出力できていました。
Filter2D_accel関数の出力であるout_img.jpgも差分処理した画像が出力されていました。
ラプラシアンフィルタを用いることによって、画素値が大きく変わるエッジ部分のみが強調されています。
誤差がないので当然ですが、参照画像との差分も問題なく真っ黒の画像が得られていました。
まとめ
Filter2D_accel関数のCシミュレーションを行い、無事に所望の機能が得られることが確認できました。
引き続き、次の記事では高位合成を行った結果や協調シミュレーションの結果を示していきたいと思います。
最後までご覧いただきありがとうございました!
コメント