画像認識の前に画像を整える ―― 前処理の三つの型と、点処理を仮想FPGAで確かめる

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

カメラで撮った画像をそのまま認識器に入れても、うまく当たらないことがあります。明るさや向き、大きさ、ノイズが撮るたびに変わるからです。認識の前に、そのばらつきを整えておきます。これが前処理で、地味ですが認識の精度を底上げする工程です。

私はこれから画像処理をちゃんとやりたくて、まず前処理から手を動かして確かめることにしました。この記事はその記録で、一緒に手探りするつもりで書いています。

前処理でよく使う操作は、たくさんあるようでいて、画素をどう見るかで三つの型に分かれます。

  • 点処理。画素 1 個を、その値だけで別の値に写します。位置には依りません。ガンマ補正やコントラスト調整がこれです。
  • 近傍処理。ある画素とその周りをまとめて計算します。平滑化やエッジ抽出に使います。
  • 幾何処理。画素の値ではなく位置を作り直します。回転や拡大縮小、位置合わせがこれにあたります。

この三つは、計算の形が違うだけでなく、FPGA に載せたときの回路の作りも根本から変わります。カメラから流れてくる画素を止めずに、認識器の手前で整えたいとき、FPGA はちょうどよい置き場所です。そこでこの連載では、三つの型を一つずつ実際の画像に通しながら、Python の理想の計算と、合成可能な Verilog をシミュレータで回した「仮想 FPGA」の両方で確かめていきます。

目的は二つです。前処理そのものを自分の手で理解すること。それと、ハードで動かすときに出てくる固定小数点の丸めや色深度の違いが、理想の計算からどれだけずれるかを、実際の画像で数字にして見ることです。

使う画像は、scikit-image に同梱されている astronaut にしました。原典は NASA が公開しているパブリックドメインの写真です。

点処理から始める

三つのうち、いちばん素直なのが点処理です。画素 1 個を受け取って、その値だけで出力を決めます。周りの画素も、その画素がどこにあるかも関係ありません。

例に選んだのはガンマ補正です。暗いところを持ち上げて、認識器が影の中の形を拾いやすくする前処理を想定します。入力を 0 から 1 に正規化した値 x とすると、出力は x の γ 乗です。γ を 1 より小さくすると暗い側が持ち上がります。ここでは γ を 0.5 にしました。

横軸が入力値、縦軸が出力値の対応曲線。灰色の点線は入力と出力が等しい直線で、赤い曲線がガンマ0.5の対応を表す。赤い曲線は点線より上にふくらみ、とくに入力の小さい暗い側で出力が大きく持ち上がっている

この対応は、入力の値ごとに出力の値が一つ決まるだけなので、あらかじめ全部計算して表にしておけます。8bit なら入力は 0 から 255 の 256 通りしかないので、256 個の出力を並べた表を引くだけで済みます。これがルックアップテーブル(LUT)で、FPGA では小さなメモリ 1 個に収まります。位置に依らない点処理は、この表引き一発になるのが強みです。

点処理には、一つ都合のよい性質があります。8bit 入力に対して 8bit 出力の LUT は、とりうる 256 通りの入力それぞれについて float で計算した値を丸めて並べた表なので、同じ式の理想値(float)と差が出ません。実際、同じ画像で LUT の結果と float の結果を比べると、差は 0 でした。近傍や幾何ではこうはいかず、丸めの誤差が残ります。それは次回以降で見ていきます。

仮想 FPGA と Python を突き合わせる

この連載の確かめ方を、点処理を例に決めておきます。

まず Python でガンマの LUT(256 個の 8bit 値)を作り、テキストの hex ファイルに書き出します。Verilog 側は、その表を $readmemh で読み込み、入力画素を表引きするだけのモジュールです。テストベンチが画像の画素を 1 個ずつ入れて、出力を CSV に書き出します。リアルタイムのタイミングは作らず、処理だけをバッチで回します。

あとは、Verilog が書いた CSV と、Python で同じ LUT を引いた結果を突き合わせます。同じ表を引いているのでビット単位で完全に一致するはずで、実際に一致しました。当たり前に思えますが、これを毎回確かめておくと、固定小数点の実装を書き換えたときに理想からずれていないかを、画像全体で機械的に点検できます。

左は処理前の入力画像で、宇宙服を着た宇宙飛行士のポートレート。右はガンマ0.5をかけた結果で、宇宙服の影やヘルメット内部といった暗い部分が持ち上がり、細部が見えるようになっている

左が入力、右が仮想 FPGA がガンマ補正した結果です。暗部が持ち上がって、認識器に渡す前に影の中の情報が見えるようになりました。

入り口は 565、演算は 888

もう一つ、実機を意識した作法を最初に決めておきます。

多くのカメラセンサや表示は、色を RGB565、つまり赤 5bit・緑 6bit・青 5bit の 16bit で扱います。緑が 1bit 多いのは、人の目が緑の違いに敏感だからです。8bit ずつの RGB888(24bit)より色の刻みは粗いぶん、メモリと帯域が 3 分の 2 ほどで済みます。だから入り口と格納は 565 が現実的です。

ただ、演算まで 565 の粗さでやると、あとで困ります。ここで LSB は、8bit の値(0 から 255)の 1 目盛りぶんの差、つまり最小の刻み 1 つを指します。試しに、565 で受け取った画像をそのままガンマ補正した結果と、元の 888 のままガンマ補正した結果を比べると、暗部を持ち上げたところで最大 42 LSB、平均で 2.9 LSB ずれました。ガンマは暗い側で傾きが急なので、入り口の粗い刻みがそこで広げられて、滑らかなグラデーションに段が見えることがあります。

そこでこの連載では、格納と入り口は 565 でも、演算は 888 に広げてから行うことにします。565 から 888 へ戻すときは、欠けた下位ビットを上位ビットで埋めます。今回の入力画像も、いったん 565 にしてから 888 に広げた、実機が受け取る形にしてあります。

次に向けて

点処理は、位置に依らない表引きでした。次は近傍処理、周りの画素をまとめて計算するガウシアン平滑化を、ラインバッファと固定小数点で組んで、同じように仮想 FPGA と突き合わせます。近傍からは、丸めの誤差や画像の端の扱いが出てきます。

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

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

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