画像認識の前処理・幾何の型 ―― アフィン変換をバイリニアと固定小数点で確かめる

  • 画像処理
  • fpga
  • verilog
  • 組み込み
  • python

点処理は画素の値を、近傍処理は画素と周りの値を扱いました(第 1 回 点の型 / 第 2 回 近傍の型)。今回の幾何処理は、値ではなく位置を作り直します。回転や拡大縮小、平行移動、せん断。認識の前処理では、傾いた被写体を正面に直したり、いつも同じ位置と大きさにそろえたりする位置合わせに使います。

アフィン変換は、この回転・拡大縮小・平行移動・せん断(長方形を平行四辺形に傾ける変形)を、ひとまとめの式で書ける変換です。例はその中の回転にしました。画像を中心まわりに 20 度回します。

逆写像で作る

はじめに思いつくのは、入力の各画素を回した先へ置いていくやり方です。でもこれをやると、出力に画素の来ない穴が空いたり、逆に複数が同じ場所へ重なったりします。回転や拡大では、格子が格子のまま移らないからです。

そこで逆から作ります。出力の各画素について、それが元画像のどこから来たのかを逆回転で求め、その場所の値を取ってきます。こうすれば出力の全画素が、ちょうど 1 回ずつ埋まります。これが逆写像です。幾何処理はたいていこの向きで組みます。

左は処理前の画像で、宇宙飛行士のポートレートがまっすぐ写っている。右はそれを中心まわりに20度回転させた結果で、四隅は元画像の外側にあたるため黒く埋まっている

左が入力、右が、合成可能な Verilog をシミュレータで回した「仮想 FPGA」の出力です。四隅は元画像の外側にあたるので、黒で埋めました。画像は連載を通して同じで、scikit-image に同梱の astronaut(原典は NASA のパブリックドメイン写真)を、565 で受けてから 888 に広げて使っています。

途中に落ちる座標を補間する

逆写像で元の座標を求めると、たいてい整数になりません。出力の (100, 100) が、元画像の (87.3, 152.6) から来る、といった具合です。そこに画素はないので、周りの 4 画素から作ります。

小数部で重みをつけて、上下左右の 4 画素を混ぜます。横の小数部を fx、縦を fy とすると、左上の重みは (1-fx)(1-fy)、右上は fx(1-fy)、というふうに、近い画素ほど重くなります。4 つの重みの和はちょうど 1 です。このように混ぜるのがバイリニア補間です。

4つの画素が正方形の四隅に置かれ、その内側の一点が元座標として赤い点で示されている。横方向の小数部fxと縦方向の小数部fyで、4画素をどれだけ混ぜるかが決まることを表している

図の赤い点が、逆写像で求めた元の座標です。この点が四隅のどの画素に近いかを fx と fy が表していて、近い画素ほど重く混ざります。

ハードにするときは、小数部を固定小数点で持ちます。固定小数点は、小数を整数と決まったスケール(何倍か)の組で表すやり方です。fx と fy を 8bit の小数、つまり 256 を 1.0 とみなして持つことにすると、(1-fx) にあたる重みは 256-fx とそのまま整数で書けます。

縦横の重みを掛け合わせると 16bit 分になるので、最後に割る数は 256×256 の 65536、16bit の右シフト 1 回です。近傍のときにシフトの前に半分ぶんを足して四捨五入にしたのと同じ手で、ここでは 32768 を足してから落とします。この 4 画素の重み付き和が、Verilog の補間モジュールになります。

座標を作る精度が効く

点処理や近傍処理では気にせずに済んだのが、元の座標を作る計算の精度です。

回転の座標は cos と sin を使って作ります。ここで効くのは、補間の重みとは別の話で、回転の係数そのものを何 bit で持つかです。cos と sin を小数部 8bit の固定小数点(小数部のビット数をとって Q8 と書きます)で持ったときと、Python が float で計算した理想とを比べてみました。

左はcos/sinを8bitの小数で持ったときの、float理想との差を明るさで表した誤差マップ。画像の輪郭に沿って広く明るく光っており、最大の差は169に達する。右は12bitの小数で持ったときの同じ誤差マップで、同じ目盛りで描いているため全面が真っ黒に見え、最大の差は2にとどまる

二枚とも、float の理想との差の大きさを明るさにした図です。明るいほど差が大きく、左右は同じ目盛りで並べています。

左が Q8 のときの誤差です。画像の輪郭に沿って広く光っていて、最大で 169 LSB ずれました。LSB は 8bit の値(0 から 255)の 1 目盛りぶん、つまり最小の刻み 1 つのことです。

cos と sin のわずかな誤差は、回転の中心から遠い画素ほど大きな座標のずれになって積もります。ただし座標が少しずれても、のっぺりした面では拾ってくる値がほとんど変わりません。差が見えるのは、隣どうしで明るさが急に変わる輪郭のところです。だから誤差マップは、面ではなく輪郭に沿って光ります。

右は cos と sin の小数部を 12bit に増やした Q12 のときです。見てのとおり、真っ黒になりました。

念のため書いておくと、これは図が出ていないのではなく、これがそのまま結果です。左と同じ目盛りで描いているので、最大でも 2 LSB しかない誤差は黒と見分けがつきません。誤差がゼロになったわけではなく、輪郭のところに 2 LSB ぶんは残っています。ただ、左を測ったのと同じものさしを当てるかぎり、それは目に見える量ではない、ということです。

係数の精度をここまで上げると、あとは何も見えなくなる。この黒さが、そのまま答えでした。

点処理は差が 0、近傍は最大 1 LSB でした。幾何は、座標を作る係数の精度が足りないとここまで大きくずれます。

今回の最終的な実装では、cos と sin を Q12 で持ち、補間の小数部を Q8 にしました。突き合わせ方は前の 2 回と同じで、Verilog のテストベンチが画素ごとに書き出した CSV と、Python で同じ整数の手順をなぞった結果を、全画素で比べます。この構成で両者はビット単位で一致し、float の理想との差も最大 2 LSB に収まりました。

どこまで前処理するか

実務でどこまで前処理に手をかけるかは、いつも迷うところです。

前処理は、やればやるほど良いわけではありません。ばらつきを前処理で抑えるか、それとも学習側で吸わせるかは、状況で変わります。

一般的な画像認識で学習データが大量にあるなら、リサイズと正規化くらいにとどめ、あとは学習のときにわざと明るさや向きを振ったデータを見せて、モデルにばらつきごと覚えさせる作り方が主流です。前処理は薄くなりがちです。

一方、産業の検査のように、データが少なく、照明も被写体の位置も自分で決められる現場では逆です。前処理でばらつきを抑えておくほうが、少ないデータで安定して当たります。毎回同じ構図に位置合わせし、照明むらを補正し、必要なら平滑化しておく。今回見た三つの型は、ちょうどこの骨格にあたります。

どちらにしても、認識に効かない処理を足すのは避けたいところです。私はまず、自分の問題でどの型がどれだけ要るかを見極めてから、実装に入るようにしています。

まとめ

点・近傍・幾何と見てきました。点処理は位置に依らない表引きで、LUT 一発、8bit なら誤差は 0 でした。近傍処理はラインバッファで窓を作る畳み込みで、差は最大 1 LSB。しかも中身は丸めの同点の決め方の違いで、固定小数点そのものの誤差ではありませんでした。幾何処理は逆写像とバイリニア補間で、ここだけは座標を作る係数の精度が効き、Q12 にして差が最大 2 LSB に収まりました。

同じ画像を通しても、回路の作りも、固定小数点で気をつける場所も、型ごとに違いました。数字で並べてみるまで、私はこの差をここまではっきりとは意識していませんでした。

ここまで作ったのはどれも処理の中身だけで、カメラの画素速度に間に合わせるためのパイプライン化やタイミング設計には手をつけていません。三つの型を並べたうえで、そこをどう詰めるかが次の話になります。

コードと Verilog、図を作るスクリプトは GitHub に置きました(logicia32/image-proc-lab)。ここで扱ったのはどれも実機の実測ではなく、簡単なモデルを Python と合成可能な Verilog のシミュレーションで確かめたものです。

この連載は全 3 回です。第 1 回 点の型第 2 回 近傍の型、第 3 回(幾何の型・この記事)。

この記事は Zenn に初出したものを加筆・補足したものです ── Zenn の元記事を見る