[C/C++]opencvのcv::Mat型を配列に変換して様々な処理をしよう![コード付]

C言語

どうもー
C++で画像処理をするならopencvを用いるのは必須だと思います。
その際にできる限りコードを書きやすくできるような画像読み書きの関数を紹介したいと思います。

まあコレまでの画像処理の記事内ではぬるっと使っていました。
よろしければこちらの記事もどうぞ↓

前提的なもの

前提として僕はXcodeでコードを書いて、実行しています。
opencvはhomebrewからダウンロードして、xcodeからパスをつないだり必要なファイルなどをブチ込んだり、、、とバンバンに使える状態から始めています。
Xcodeにopencvを導入する方法はgoogkeで調べればいくらでもあるので、まだ導入できてない人はまずそこをお願いします。(気が向いたらこのサイトでもopencvの導入の記事を書くかも

cv::Mat型の基本情報

まずここから、
opencvで画像を格納する型としてcv::Mat型が定義されています。
これは標準の配列などと違って画像を保存することに特化した形になっています。
以下のような形で使います。

#define HEIGHT 480
#define WIDTH 640

cv::Mat IMAGE;
IMAGE = cv::imread("read.png", 1);
cv::imwrite("write.png" , IMAGE);

1行目が定義ですね。普通の変数と同じです。
2行目ではcv::imreadという関数を用いて画像を読み込んでcv::Matに格納しています。
引数の一つ目が読み込む画像の名前で二つ目がどのように読み込むかです。(0ならグレースケール、1ならカラー画像)
3行目はcv::imwriteという関数でcv::Matに格納された画像を書き込んでいます。

cv::Mat型の画素のアクセス

ここからが本題ですね。
普通の配列の場合、ある要素にアクセスするときは

#define HEIGHT 480
#define WIDTH 640
#define color

int image[HEIGHT][WIDTH][color] = {0}
//
cout<<image[100][200][0]<<endl;

こんな感じですね。
まあ何の変哲もないですし慣れていると思います。

ここでcv::Mat型も2次元の情報である画像を格納している型なので同じように画素にアクセスできるかと思いきやできないんですよね。
普通のint型の2次元配列ではなく、あくまでcv::Mat型なので専用のアクセス方法があります。
それがこちら

#define HEIGHT 480
#define WIDTH 640

cv::Mat IMAGE;
IMAGE = cv::imread("read.png", 1);

//座標(100,200)のBの値を出力
cout<<IMAGE.at<cv::Vec3b>(100,200)[0]<<endl;

このようにcv::Mat型にはatメソッドが定義されており

Image.at<cv::Vec3b>(y.x)

とすることで(y,x)座標の画素にアクセスできます。

配列よりも書く量が多いね、、

そうなんですよね〜
それに加えて問題なのがatメソッドは処理が遅いということです。
少しの画素を参照だったり変更するぐらいなら良いですが、全ての画素に対して何やかんやするとなると少し実行時間などが気になってきますね。

てことで今回のメインはcv::Mat型からint型の二次元配列に変換して色々処理をしよう!って話です。

配列の形のほうが慣れているし作業もしやすいですよね、

dataメソッドを用いて配列に代入しよう!

実はcv::Mat型の画素にアクセスする方法としてdataメソッドというものがあります。

#define HEIGHT 480
#define WIDTH 640

cv::Mat IMAGE;
IMAGE = cv::imread("read.png", 1);

//座標(100,200)のBの値を出力
cout<<IMAGE.data[]

何が起きているかというと、
Image.data[取得したい画素の格納場所]

のようになっいます。
取得したい画素の格納場所となっていますが、正確にはその画素がcv::Mat型の中で先頭から何番目の画素か?ということです。
画素は以下のように並んでいます、

つまり座標(y,x)の画素のR値が欲しければ、

Image.data[y*Image.cols + x + 0]

とすれば良いわけです。
まあこんな感じでdataメソッドは引数が1つしかない分、atメソッドに比べて1点の画素のアクセスが難しいですよね。
しかしatメソッドと比べて処理が軽いので、ラスタ走査などと組み合わせて全画素にアクセスすることが得意です。

画像を読み込んで配列に格納する関数

はいー
では画像を読み込んで配列に格納する関数を紹介します。
この関数さえあれば画像を普段の配列の操作、処理で色々できるわけですね。
ではコードです。

#define HEIGHT 480
#define WIDTH 640

void Read_Image( const char *name, unsigned char img[HEIGHT][WIDTH][3] )
{
    cv::Mat readImage( HEIGHT, WIDTH, CV_8UC3);
    readImage = cv::imread(name, 1);

    for( int w=0; w<WIDTH; w++)
        for( int h=0; h<HEIGHT; h++)
            for( int c=0; c<3; c++)
                img[h][w][c] = readImage.data[(w+h*WIDTH)*3+c];
}

まず引数について、
一つ目のconst char *nameは読み込みたい画像の名前を渡す部分ですね。

二つ目のunsigned char img[HEIGHT][WIDTH]は画像を格納する配列です。
HEIGHTとWIDTHはマクロ定義する画像の大きさです。(ここでは480×640にしているけど何でも良い)
正味ここは画像を読み込んでからそのサイズをcolsやrowsなどで取得してもおkです。

また型をunsigned charにしているのは、charがもともと±127までの数値を扱えて、unsigned にすることで0〜255まで扱えるようになります。これはちょうど画素値の大きさの範囲と同じなんです。

そして本文です。
最初はcv::Mat型でReadImageという変数を定義して、そこにcv::imreadで画像を読み込んでいます。この時の引数がこの関数の引数で取得したnameですね。

その次がcv::Matを配列に変換する部分です。
まず外側の2重forループで画像の画素一つ一つをラスタ走査しています。
その中でさらにfor( int c=0; c<3; c++)としてます。コレはRGBを表しているってことですね。

その3重ループの中で

img[h][w][c] = readImage.data[(w+h*WIDTH)*3+c];

このようにimgの中にreadImageの値をdataメソッドを利用して代入しています。
この時の引数の(w+h*WIDTH)3+cは上で説明したdataメソッドにそって書かれています。

この関数の処理が終わると結果的に、
一つ目の引数で指定した画像が、二つ目の引数で渡した配列に格納される
わけです。便利ですね〜。

配列に格納された画像をファイルに書き出す関数

今度は書き出しの関数です。
上の関数のように、cv::imwriteを二次元配列用に改造する感じですね。

#define HEIGHT 480
#define WIDTH 640

void Write_Image( const char *fname, unsigned char img[HEIGHT][WIDTH][3] )
{
    cv::Mat writeImage( HEIGHT, WIDTH, CV_8UC3 );

    for( int w=0; w<WIDTH; w++ )
        for( int h=0; h<HEIGHT; h++ )
            for( int c=0; c<3; c++ )
                writeImage.data[(w+h*WIDTH)*3+c] = img[h][w][c];
    
    cv::imwrite( fname , IMAGE);
}

引数は上の関数と全く同じですね。
中身も先ほどと逆の手順を行っているだけです。
3重for文を用いて、配列の中身をcv::Mat型のwriteImageに格納しています。

最後はcv::imwriteを用いてファイルに書き込んでいます。

まとめ

以上が画像を配列に格納、また配列に入った画像の書き出しの関数です。
中盤でも語りましたが、cv::Mat型として計算するのも良いですがやはり配列の方が慣れていますし都合が良い時があると思います。

そんな時に上の関数をバンバン使ってください!
ではまた〜


コメント

タイトルとURLをコピーしました