OpenCVのcv::Matに関する小ネタ集

はじめに

今回は、C++でOpenCVのcv::Matを使う際の小ネタ集である。以下の小ネタを扱う。

  1. 画素へのアクセス
  2. BGRの謎
  3. 演算子のオーバーロード
  4. ROIの抽出
  5. Blobの作成

cv::Matとは、画像を保持する2\times2の行列を表すクラスである。

検証環境

  1. OS: Windows11 Pro
  2. プロセッサ: Intel(R) Core i7 2.80 GHz
  3. RAM: 16GB

1. 画素へのアクセス

最初に取り上げる話題は画素へのアクセスの仕方だ。画像の各画素にアクセスする方法として次の3つを考えることができる。

  • atメソッドを使う。
  • ポインタを使う。
  • イテレータを使う。

順にサンプルコードを見ていく。

atメソッドを使う

4行目と5行目で入力画像の行数rowsと列数colsを取得している。8行目で出力画像を格納する箱を用意する。CV_8UC3は符号なし8ビット・3チャンネルを指定する識別子である。ループの中では全画素値の置き換えをしている。13行目と26行目でcv::Matのatメソッドを使っている。atメソッドの呼び出しごとに境界チェック(画像外をアクセスしていないか)が行われるため、実行速度は遅くなる。

ポインタを使う

cv::Matのptrメソッドを使い、9行目で入力画像のポインタを、10行目で出力画像のポインタを取得している。この2つのポインタはどちらもj行目の画像の先頭を指す。12行目と25行目でi列目の画素値にアクセスしている。ループの中でしていることはatメソッドのときと同じである。アクセス時に境界チェックは行われないので高速である。ただし、画像のメモリレイアウトを正確に把握しておく必要がある。

イテレータを使う

8行目から10行目でcv::Matのbeginメソッドとendメソッドを使いイテレータを取得している。これを使い左上から右下に向けて直線的に画素にアクセスする。イテレータの場合はatメソッドの時と同様に境界チェックが行われるためオーバヘッドがある。

速度比較

3000\times3000の画像の全画素の更新を100回行い平均を取った処理速度は以下の通り。予想通りポインタ版が最も速い。イテレータ版が最も遅いのは少し意外であった。

図1

2. BGRの謎

次の話題は「なぜOpenCVの画素の並びはRGBではなくBGRなのか」だ。

9行目から11行目でBGRの順に値を取り出している。この並びである理由の1つとして、OpenCVの前身であるIPL(Image Processing Library)での並びがBGRであったためであると言われている。IPLはもともとWindows用であり、Windowsでは内部画像形式としてDIB(Device Independent Bitmap)が使われていた。DIBの画素の並びはBGRである。他にもいろいろな説があるので興味がある方はこちらを参照してほしい。

3. 演算子のオーバーロード

次の話題は演算子のオーバーロードである。以下のコードに見るようにcv::Matのインスタンスを普通の数字のように使い四則演算を行うことができる。

3行目と4行目で2\times2行列xyを作成している。xの成分は全て1に、yの成分は全て2に初期化している。6行目以降に示した3つの演算子「+」「-」「*」の実装には式テンプレート(Expression Template)という手法が使われる。この手法は、式全体を1つのオブジェクトとして表現し、実際に計算が必要になるまでその評価を遅らせる方法である(遅延評価と呼ばれる)。上の例で言えば、9行目、15行目、21行目で初めて数値を用いた計算が実行される。演算「*」は行列同士の積である。出力結果は以下の通り。

図2

このテクニックにより、計算の過程で発生する一時オブジェクトの生成を避けることができ、パフォーマンスの向上を期待できる。上のコードに挙げた例以外に、割り算(/)や各種不等号もサポートされる。サポートされる演算の一覧はこちらのAPIドキュメントを見てほしい。

4. ROIの抽出

次の話題はROI(Region Of Interest)の抽出である。画像内の任意の矩形領域を抽出する処理である。

18行目でROIを抽出し変数roiに代入している。抽出される画像内の場所は11行目から15行目で指定される。roiは画像内の矩形領域のコピーでなく参照であるため、21行目でこの領域を黒く塗りつぶすと元画像imageの対応する領域も黒になる。図2の左は黒く塗りつぶす前の画像、右はroiを黒く塗りつぶした後のimageである(画像はこちらからダウンロードした)。

図2

参照ではなくコピーが欲しい場合は、18行目の右辺をimage(roi).clone()とすれば良い。

5. Blobの作成

最後の話題は、深層学習の入力(Blobと呼ばれる)についてだ(OpenCVを用いた深層学習については前回のブログで解説した)。深層学習の入力は普通の画像とは異なる形式でcv::Matに格納される。

7行目作られるcv::MatのインスタンスoriginalImageは通常のフォーマットで画像を格納する。13行目から15行目で高さ、幅、チャンネル数を表示している。20行目から25行目でBlobを作成する準備をし、26行目のcv::dnn::blobFromImagesでBlobinputBlogを作成している。Blogの情報は28行目から32行目のようにして取得する。inputBlob.sizeの中に(batch_size,channels,height,width)の順に格納されている。bach_sizeが2となる理由は、25行目で2枚の画像を与えているからである。通常の画像のように情報を取得すると(34 行目から36行目)無効な値が返るので注意が必要である。画像1枚のときのメモリレイアウトを図3に示す。

図3

3つのチャンネルが別々に格納されているので、通常のレイアウトに戻すには、38行目から52行目のようにひと手間が必要である。画像表示関数cv::imshow(55行目)に渡せるのは通常のレイアウトを持つcv::Matだけである。

まとめ

今回は、C++で書く時のOpenCVの小ネタを集めた。コピーや参照、ポインタなどの振る舞いはPythonではあまり表に出てこないが大切な概念である。また、深層学習を行う際はcv::Matへの画像格納方法が通常と異なることに注意しなければならない。Python版のOpenCVであれば画像は全てnumpyのarrayに格納されるので、画像処理と深層学習の間で画像の持ち方に大きな違いはない。後者の場合にバッチサイズ用に次元が1つ増えるだけである。

参考文献

Kumada Seiya

Kumada Seiya

仕事であろうとなかろうと勉強し続ける、その結果”中身”を知ったエンジニアになれる

最近の記事

  • 関連記事
  • おすすめ記事
  • 特集記事

アーカイブ

カテゴリー

PAGE TOP