![]() |
OpenCV 5.0.0
Open Source Computer Vision
|
前のチュートリアル: Mat - 基本的な画像コンテナ
次のチュートリアル: 行列に対するマスク操作
| 原著者 | Bernát Gábor |
| 互換性 | OpenCV >= 3.0 |
次の問いに対する答えを探す:
単純な色数削減の手法を考えてみる。行列の要素を格納するために C および C++ の unsigned char 型を使うと、ピクセルの1チャンネルは最大256個の異なる値を取りうる。3チャンネル画像ではこれにより非常に多くの色(正確には1600万色)を構成できる。これほど多くの色合いを扱うとアルゴリズムの性能に大きな打撃を与えかねない。しかし、同じ最終結果を得るには、もっと少ない色数で作業すれば十分な場合がある。
このような場合、色空間の削減を行うのが一般的である。これは、現在の色空間の値を新しい入力値で割って、より少ない色数にすることを意味する。例えば、0から9までのすべての値は新しい値0を取り、10から19までのすべての値は値10を取る、というようになる。
uchar(unsigned char、すなわち0から255までの値)の値を int の値で割ると、結果も char になる。これらの値は char 値のみを取りうる。したがって、端数はすべて切り捨てられる。この事実を利用すると、uchar 領域における上記の演算は次のように表せる。
\[I_{new} = (\frac{I_{old}}{10}) * 10\]
単純なカラー空間削減アルゴリズムは、画像行列の全ピクセルを順に処理し、この式を適用するだけのものになる。ここで除算と乗算を行っている点に注目したい。これらの演算はシステムにとって非常にコストが高い。可能であれば、いくつかの減算、加算、あるいは最良の場合は単純な代入といった、より安価な演算を用いてこれらを避ける価値がある。さらに、上記の演算で扱う入力値の数は限られていることに注意する。uchar システムの場合、正確には256個である。
したがって、より大きな画像の場合は、可能なすべての値をあらかじめ計算しておき、代入時にはルックアップテーブルを使って代入だけを行うのが賢明である。ルックアップテーブルは、与えられた入力値の各バリエーションに対して最終的な出力値を保持する単純な配列(1次元以上)である。その強みは、計算を行う必要がなく、結果を読み出すだけでよい点にある。
本テストプログラム(および以下のコードサンプル)では次のことを行う。コマンドライン引数として渡された画像(カラーまたはグレースケールのいずれでもよい)を読み込み、与えられたコマンドライン引数の整数値で削減を適用する。OpenCVには現在、画像をピクセル単位で走査する主要な方法が3つある。少し面白くするために、これらの各方法で画像を走査し、それぞれにかかった時間を出力する。
完全なソースコードは こちら からダウンロードできる。または、OpenCV の samples ディレクトリにある core セクションの cpp チュートリアルコードで参照できる。基本的な使い方は次の通り:
最後の引数は省略可能である。指定すると画像はグレースケール形式で読み込まれ、指定しない場合はBGRカラー空間が使われる。最初に行うのはルックアップテーブルの計算である。
ここではまずC++のstringstreamクラスを使って、3番目のコマンドライン引数をテキストから整数形式に変換する。続いて、単純なループと上記の式を使ってルックアップテーブルを計算する。ここにはOpenCV固有の処理はない。
もう一つの問題は、どうやって時間を計測するかである。OpenCVはこれを実現するための2つの単純な関数、cv::getTickCount() と cv::getTickFrequency() を提供している。前者は、特定のイベント(システムを起動した時点など)からのシステムCPUのティック数を返す。後者は、CPUが1秒間に何回ティックを発するかを返す。したがって、2つの演算の間に経過した時間を計測するのは次のように簡単である。
Mat - 基本的な画像コンテナのチュートリアルですでに読んだとおり、行列のサイズは使用するカラーシステムに依存する。より正確には、使用するチャンネル数に依存する。グレースケール画像の場合は次のようになる。
マルチチャンネル画像では、列はチャンネル数と同じ数のサブ列を含む。例えばBGRカラーシステムの場合は次のようになる。
チャンネルの順序が逆である点に注意する。RGBではなくBGRである。多くの場合、メモリは行を連続して格納できるほど十分に大きいため、行は次々と連続して並び、1本の長い行を形成することがある。すべてが1か所に次々と連続して並んでいると、走査処理の高速化に役立つことがある。行列がこの状態かどうかを問い合わせるには、cv::Mat::isContinuous() 関数を使える。次のセクションに進むと例が見つかる。
性能に関しては、古典的なC言語スタイルの operator[] (ポインタ) アクセスに勝るものはない。したがって、代入を行うために推奨できる最も効率的な方法は次のとおりである。
ここでは基本的に各行の先頭へのポインタを取得し、行末まで処理を進めるだけである。行列が連続して格納されている特別な場合には、ポインタを一度だけ取得して最後まで進めばよい。カラー画像には注意が必要である。3つのチャンネルがあるため、各行で3倍の数の要素を処理する必要がある。
これには別のやり方もある。Mat オブジェクトのdataデータメンバは、先頭行・先頭列へのポインタを返す。このポインタがnullの場合、そのオブジェクトに有効な入力はない。これを確認することは、画像の読み込みが成功したかを調べる最も簡単な方法である。格納が連続している場合、これを使ってデータポインタ全体を走査できる。グレースケール画像の場合は次のようになる。
同じ結果が得られる。しかし、このコードは後から読むのがかなり難しくなる。より高度な手法を使うと、さらに難しくなる。さらに、実際には同じ性能結果になることを観察している(ほとんどの最新コンパイラは、おそらくこの小さな最適化トリックを自動的に行ってくれるため)。
効率的な方法では、適切な数のucharフィールドを確実に処理し、行間に生じうる隙間をスキップするのは利用者の責任だった。イテレータ方式はこれらの作業を利用者から引き受けるため、より安全な方法と考えられている。必要なのは、画像行列の先頭と末尾を求め、末尾に到達するまで先頭イテレータをインクリメントしていくことだけである。イテレータが指す値を取得するには、* 演算子を(その前に付けて)使う。
カラー画像の場合、列ごとに3つのucharの要素がある。これはucharの要素からなる短いベクトルとみなせ、OpenCVではVec3bという名前が付けられている。n番目のサブ列にアクセスするには、単純な operator[] アクセスを使う。OpenCVのイテレータは列を順に走査し、自動的に次の行へ移ることを覚えておくのが重要である。したがって、カラー画像で単純なucharイテレータを使うと、青チャンネルの値にしかアクセスできない。
最後の方法は走査には推奨されない。画像中のランダムな要素を何らかの方法で取得または変更するために作られたものである。基本的な使い方は、アクセスしたい要素の行番号と列番号を指定することである。これまでの走査方法で、どの型を通して画像を見ているかが重要であることにすでに気づいたかもしれない。ここでも同様で、自動ルックアップで使用する型を手動で指定する必要がある。グレースケール画像の場合、次のソースコードでこれを確認できる(+ cv::Mat::at() 関数の使用)。
この関数は入力の型と座標を受け取り、問い合わせた要素のアドレスを計算する。そしてその参照を返す。値を取得する場合は定数参照、値を設定する場合は非定数参照になる。安全策としてデバッグモードでのみ*、入力座標が有効で実在するかどうかのチェックが行われる。そうでない場合は、標準エラー出力ストリームにこれを示す適切な出力メッセージが表示される。リリースモードでの効率的な方法と比べると、これを使う際の唯一の違いは、画像の各要素について新しい行ポインタを取得し、それに対してC言語の operator[] を使って列要素を取得する点である。
この方法で1つの画像に対して複数回のルックアップを行う必要がある場合、各アクセスごとに型と at キーワードを入力するのは面倒で時間がかかる。この問題を解決するため、OpenCV には cv::Mat_ データ型がある。これは Mat と同じだが、定義時にデータ行列を参照する際のデータ型を指定する必要があり、その代わりに operator() を使って要素に高速アクセスできる。さらに都合のよいことに、これは通常の cv::Mat データ型と容易に相互変換できる。この使用例は、上記関数のカラー画像の場合に見ることができる。ただし、同じ操作(同じ実行速度で)は cv::Mat::at 関数でも実行できる点に注意することが重要である。これは怠惰なプログラマ向けに記述量を減らすための仕掛けにすぎない。
これは画像中のルックアップテーブル変更を実現する付加的な方法である。画像処理では、与えられた画像のすべての値を別の値に変更したいことがよくある。OpenCVは、画像の走査ロジックを書く必要なく画像の値を変更する関数を提供している。ここではcoreモジュールのcv::LUT() 関数を使う。まず、ルックアップテーブルをMat型として構築する。
最後に関数を呼び出す(Iは入力画像、Jは出力画像)。
最良の結果を得るには、プログラムをコンパイルして自分で実行するとよい。違いをより明確にするため、かなり大きな(2560 X 1600)画像を使った。ここで示す性能はカラー画像のものである。より正確な値を得るために、関数を100回呼び出して得た値を平均した。
| 方法 | 時間 |
|---|---|
| 効率的な方法 | 79.4717ミリ秒 |
| イテレータ | 83.7201ミリ秒 |
| オンザフライRA | 93.7878ミリ秒 |
| LUT関数 | 32.5759ミリ秒 |
いくつかの結論が導ける。可能であれば、OpenCVの既製の関数を(これらを再発明するのではなく)使う。最速の方法はLUT関数であることがわかる。これは、OpenCVライブラリがIntel Threaded Building Blocksによってマルチスレッド対応しているためである。ただし、単純な画像走査を書く必要がある場合は、ポインタ方式を優先するとよい。イテレータはより安全な選択だが、かなり遅い。フル画像走査にオンザフライの参照アクセス方式を使うのは、デバッグモードで最もコストが高い。リリースモードではイテレータ方式に勝つこともあれば勝たないこともあるが、その代わりにイテレータの安全性という特性を確実に犠牲にする。
最後に、プログラムの実行サンプルを、当チャンネルのYouTubeに投稿された動画で見ることができる。