はじめに
OpenCVを使っていても画素を直接いじることはよくあることだと思います。
先日,友人からMat型は遅いと言われました。しかし,私はポインタでガツガツいじっており,経験的に遅くないと考えておりました。そこで,今回,様々な方法で画素へアクセスし,スピードを比較してみることに至ったのです。(それと自分へのメモ用)
また,後輩からQiitaを勧められ記事をアップする練習も兼ねて書きます。
OpenCVによる画素へのアクセス
OpenCV2.X系以降,Matクラスを使用して画像を扱ってい人が多いと思います。Matクラスにある画素値へ直接操作できるメソッドを使ったり,また,Iteratorを用いて操作するなど様々な方法があります。
ここでは,考えられる方法で比較しようと思います。
cv::Mat::at<>を用いる方法
座標を指定することで,その座標の画素値にアクセスできます。
例えば,カラー(24bit)の画像imageにおいて,座標(i,j)の画素値にアクセスする場合は,次のようになります。
image.at<cv::Vec3b>(j,i)[0] //Blue image.at<cv::Vec3b>(j,i)[1] //Green image.at<cv::Vec3b>(j,i)[2] //Red
ここで,Vec3bとは,uchar型の3チャネルを表しています。
cv::MatIterator_<>を用いる方法
C++標準STLのイテレータのように画素値にアクセスできます。
例えば,カラー(24bit)の画像imageを,真っ白(すべての画素値を255)にする場合の例を下に示します。
cv::MatIterator_<cv::Vec3b> *it = image.begin<cv::Vec3b>() cv::MatIterator_<cv::Vec3b> *it_end = image.end<cv::Vec3b>() for(; it!=i_end; it++) { (*it)[0] = 255; //Blue (*it)[1] = 255; //Green (*it)[2] = 255; //Red }
Matクラスのdataを直接操作する方法
dataはデータへのポインタです。1次元配列のようにアクセスすることができます。
例えば,カラー(24bit)の画像imageを,真っ黒(すべての画素値を0)にする場合の例を下に示します。
int width = image.cols; int height = image.rows; int channels = image.channels(); for(int j=0; j<height; j++) { int step = j*width; for(int i=0; i<width; i++) { int elm = i*image.elemSize(); for(int c=0; c<channels; c++) { image.data[step + elm + c] = 0; } } }
cv::Pointクラスを用いる方法
ネット上で検索すると意外とでてくる方法です。Mat型をMat_型に変換し,Pointクラスを用いてアクセスします。
例えば,カラー(24bit)の画像imageの座標(i,j)の画素値にアクセスする場合は,次のようになります。
//Mat_型に変換 cv::Mat3b ptImg = image; //cv::Mat3bはcv::Mat_<Vec3b>同じ
//座標(i,j)にアクセス ptImg(cv::Point(j,i))
ポインタを用いる方法
ポインタを用いてアクセスする方法は,行の先頭画素のポインタを取得しアクセスします。
例えば,カラー(24bit)の画像imageの座標(i,j)の画素値にアクセスする場合は,次のようになります。
cv::Vec3b *src = image.ptr<cv::Vec3b>(j); //j行目の先頭画素のポインタを取得 src[i]; //i番目にアクセス
比較実験
lenna画像を読み込み,グレースケールに変換した画像を,しきい値による2値化で速度を計測しました。しきい値による2値化部分で画素へのアクセスを上で説明した方法を用いています。2値化部分を各方法で100回行い,その平均を示します。
実験環境
CPU : Intel(R) Core(TM) i7-6700K
メモリ : 16 GB
OS : Windows10 Pro 64bit
開発環境 : Visual Studio 14 (Visual Studio 2015)
コンパイルオプション : 実行速度の最大化(/O2),OpenMPなし
OpenCVのバージョン : 3.1.0
結果
方法 | 実行速度(ms) |
---|---|
cv::Mat::at<> | 0.405558 |
cv::MatIterator_<> | 0.484384 |
Matクラスのdata | 0.439926 |
cv::Pointクラス | 0.68961 |
ポインタ | 0.126713 |
圧倒的にポインタを用いる方法が速いです。しかし,驚きなのはMatに関するメソッドなどはそれほど速度が変わらないところです。
[参考]並列化による実験
OpenMPを用いて,各手法を並列化し,速度の検証を行って見ました。用いた手法は,cv::Mat::at<>,Matクラスのdata,cv::Pointクラス,ポインタの4つです。
forループのみをOpenMPで並列化しました。
実験環境
CPUやOS,開発環境は上と同じ。
結果
方法 | 実行速度(ms) |
---|---|
cv::Mat::at<> | 0.104058 |
Matクラスのdata | 0.121104 |
cv::Pointクラス | 0.235875 |
ポインタ | 0.0384459 |
こちらも圧倒的にポインタを用いた方法が高速です。
おわりに
ポインタを用いた画素へのアクセス方法が一番速いことが確認できました。
しかし,OpenMPを用いた場合,cv::Mat::at<>やdataへのアクセス速度は,OpenMPを用いいない場合のポインタでのアクセスをした場合とほぼ変わりがないことがわかりました。
追記(2016/02/17)
cv::Mat::forEach
を使う方が早いというご意見を頂きましたので,同じような実験をしてみました。
ただ,私があまり理解しておりませんので正しいコードでない可能性もあります。下に示しておきます。
image.copyTo(dst); //ココから計測 dst.forEach<uchar>([](uchar &x, const int *position) -> void { if(x >= th) x = HIGH; else x = LOW; }); //ココまで
ここで,image
はグレースケール画像,dst
は出力画像,th
はしきい値,HIGH
は255,LOW
は0となっております。
実験も同様に100回の平均を取りました。ちなみに,copyToを含めた場合も示しておきます。
方法 | 実行速度(ms) |
---|---|
cv::Mat::forEach | 0.0496262 |
cv::Mat::forEach(copyToも含む) | 0.0643653 |
若干,ポインタ(OpenMP使用)の方が速いみたいです。