画像認識の前処理・近傍の型 ―― ガウシアンをラインバッファと固定小数点で確かめる
- 画像処理
- fpga
- verilog
- 組み込み
- python
前回は、画素 1 個をその値だけで写す点処理を、LUT で確かめました。今回は近傍処理です。ある画素の出力を、その画素と周りの画素をまとめて決めます。ノイズを均したり、後段のエッジ抽出を安定させたりする、前処理の定番です。
例はガウシアン平滑化にしました。中心に近い画素ほど重く、遠いほど軽く重みをつけて周りを混ぜます。ノイズのようなランダムで細かい変動は均され、大きな形は残ります。
畳み込みという計算
近傍処理は畳み込みで書けます。小さな重みの升目(カーネル)を画像の上で滑らせ、重なった画素と重みを掛けて足します。その和が、その位置の出力です。
今回は 5x5 のガウシアンを使いました。重みは [1,4,6,4,1] を縦横に掛け合わせたもので、中心が 36 といちばん重く、外へ行くほど軽くなります。

重みの総和は 256 です。畳み込んだ和をこの総和で割って、明るさを元のスケールに戻します。総和が 256 という 2 の冪であることは、あとでハードにするとき効いてきます。

左が入力、右が仮想 FPGA が 5x5 ガウシアンで平滑化した結果です。近傍の効き方は細部で見たいので、256x256 に切り出しています。入力は第 1 回と同じ、565 で受けて 888 に広げた画像で、色のチャンネルごとに同じ処理を通しています。全体がやわらかくなり、細かい変動が均されているのがわかります。
ラインバッファという作り
点処理と近傍処理では、ハードの作りが変わります。点処理は画素 1 個を見れば出力が決まりましたが、近傍処理は上下 2 行ずつを含む 5x5 の窓が必要です。
画素はカメラから 1 行ずつ、左から右へ順に流れてきます。5x5 の窓を作るには、いま来ている行だけでなく、直前の 4 行分が手元に必要です。そこで、直前の行を蓄えておくメモリを用意します。これがラインバッファです。5x5 なら 4 本、1 行分の幅のバッファを並べ、新しい画素が来るたびに窓を 1 つずらしていきます。

こうすると、画像全体を何度も読み直さずに、流れてくる画素をそのまま窓に通せます。フレーム全体をメモリに置いて 1 画素あたり 25 回ずつ読みにいく作りに比べて、ラインバッファは 1 行分のメモリを数本置くだけで済みます。ここが、近傍処理を FPGA でやる勘所です。なお今回のシミュレーションで合成できる形にしたのは、次の節で見る畳み込みの演算部分です。窓に入れる 25 画素を集めるところは、ラインバッファそのものは組まず、その働きをテストベンチ側の動作記述で代わりに用意しています。
固定小数点で畳み込む
窓の 25 画素がそろえば、あとは重みと掛けて足すだけです。ハードでは、この計算を整数で組みます。固定小数点は、小数を「整数と、決まったスケール(何倍か)」の組で表すやり方です。ここでは重みを整数のまま掛けて足し、最後にスケールぶんをシフトで割って戻します。
重みは先ほどの [1,4,6,4,1] の外積そのままの整数で、掛けて足した和を最後に総和の 256 で割ります。256 で割るのは 8bit 右シフト 1 回なので、割り算の回路は要りません。四捨五入したいので、シフトの前に 128 を足しておきます。総和が 2 の冪になるガウシアンを選んだのは、この一手をシフトだけで済ませたいからでした。
この整数の計算を Verilog のモジュールにしました。25 個の掛け算と加算、丸めのシフトが、そのまま FPGA の演算になります。Python 側でも同じ整数の手順をなぞって、両者を突き合わせたところ、結果はビット単位で一致しました。
float で割り算まで丁寧にやった理想の結果とも比べると、差は最大でも 1 LSB、平均は 0.006 LSB でした。
この差の中身を見ると、少し面白いことが分かります。重みが整数で総和が 256 なので、畳み込んだ和は必ず整数で、それを 256 で割った値も float で正確に表せます。つまり固定小数点にしたことによる誤差は、ここでは出ていません。差が出た画素を数えたら 196608 画素中 1230 で、その全部が、割り算の結果がちょうど 0.5 になる画素でした。ハード側は 128 を足してから落とすので 0.5 を切り上げますが、比較に使った float 側の関数は 0.5 を偶数側に丸めます。食い違っているのは、その同点のときの決め方だけです。
端をどうするか
近傍処理では、画像の端で窓が画像からはみ出します。左端の画素には、その左に画素がありません。
やり方はいくつかあります。今回は、はみ出した先を端の画素の複製で埋めました。ほかに、はみ出しをゼロで埋める、端の内側だけを出力する、といった手もあります。どれを選ぶかで端の数画素の値が変わるので、Python と Verilog で同じ規則にそろえておかないと、突き合わせが端でずれます。ビット一致を確かめておくと、こういう取り決めの食い違いにも気づけます。
次に向けて
近傍処理は、ラインバッファで窓を作り、整数の畳み込みと丸めシフトで組みました。float の理想との差は最大 1 LSB で、しかもその中身は丸めの同点の決め方の違いだけでした。次は幾何処理、画素の位置を作り直すアフィン変換です。ここでは元の座標が画素の途中に落ちるので補間が要り、座標を作る計算の精度が効いてきます。
コードは前回と同じ GitHub に置いてあります(logicia32/image-proc-lab)。ここで見たのも実機の実測ではなく、簡単なモデルを Python と合成可能な Verilog のシミュレーションで確かめたものです。
この連載は全 3 回です。第 1 回 点の型、第 2 回(近傍の型・この記事)、第 3 回 幾何の型。
この記事は Zenn に初出したものを加筆・補足したものです ── Zenn の元記事を見る