LoginSignup
27
27

More than 5 years have passed since last update.

OpenCVの汎用入出力InputArray, OutputArrayの使い方(入門編)

Last updated at Posted at 2015-12-01

はじめに

これは,OpenCV Advent Calendar 2015の記事です.関連記事は,リンク先に目次としてまとめられています.

OpenCVの標準的なデータ入出力の形式は,1.0の時代ではIplImage構造体が,2.0の時つ代ではcv::Matクラスが使われてきました.そして,2.0から3.0への変換期の今では,それらがcv::InputArrayとcv::OutputArrayに代わりつつあります.ここでは,新しい入出力クラスであるInputArrayとOutputArrayを説明します.

このInputArrayとOutputArrayは,新しい型というよりも,呼び出す関数の引数にユーザが型を意識しなくても代入できるようにするためのジェネリクスプログラミングの一種です.(Proxy パターン https://ja.wikipedia.org/wiki/Proxy_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3 で作られたクラスです.)

cv::InputArrayの使い方

Matで受ける使い方(基本中の基本)

これまでのOpenCVの関数のように,cv::Matを入力とする関数を作ると,通常は以下の形になるかと思います.なお,この関数は,行列XをA,Bで積和した後に中身を表示する関数です.

void AXplusB1(Mat& A, Mat& X, Mat& B)
{
    Mat dest = A*X + B;
    cout<<dest<<endl;
}

これをcv::InputArrayを使って書き直すと以下のようになります.

void AXplusB2(InputArray A_, InputArray X_, InputArray B_)
{
    Mat A = A_.getMat();
    Mat X = X_.getMat();
    Mat B = B_.getMat();

    Mat dest = A*X + B;
    cout << dest << endl;
}

このように,Matで入力データを受けた後に,今まで通りの使い方をします.この,作られた関数内には同様の部分があるものの,InputArrayをMatで受けるという従来よりも余計な処理が追加されています.InputArray自体は汎用な型であるものの,なんら演算が定義されておらず,それどころかMatなどのように自身の要素にアクセスする手段すらもない型です.つまり,なんらかの演算や各要素にアクセスする必要がある場合は,Matなど演算可能な型で受ける必要があるからです.

なお,InputArrayは&記号がついていませんが参照渡しであり,作る側ではなく,関数を呼び出すユーザー側の知識としては,まずは,InputArrayとMat&はほぼ等価なものだと知っていればOKです.具体的には,下記のようにtypedefされているだけです.

typedef const _InputArray& InputArray;

直接操作する使い方:InputArrayだけでできること(ほとんど無い)

InputArrayのクラスをMatで受けない場合,できることは多くありません.
- Matなどの型で受けるためのgetMat()などのメソッド呼び出し
- size(), type(), depth(), channels()メソッドによる構造データの把握
- empty()メソッドによる実体があるかないかの判別
- create()メソッドによる実体の確保
- copy()メソッドによるコピー

つまり,InputArrayの表現のままでは,何らかの処理を行うことは一切出来ないと思って使いましょう.

ここまでに,InputArrayの良いところはどこにもなかったですが,何もアドバンテージがなければ使われることはありません.次からは,どうしてこれが次の世代で使われる入出力なのか説明します.

cv::InputArrayを使うメリット

1つ目:引数の中で計算することが出来る

作る側としてはめんどうになっただけですが,使う側から見てどのようなメリットがあるのでしょうか?
まず,一つ目の利点としてMatExprと呼ばれる型を受け付けるようになる点です.

このMatExpr(matrix expressionの略)の型は行列の演算結果を表しています.例えば,下記のような演算の結果がこのMatExprで受け取られます.
- -A (行列をマイナスの値にする)
- A+B, A-B, A*B, A/B, A.mul(B) (行列の演算をする)
- A.t(), A.inv() (行列の転置を取る,逆行列を取る)
(※通常使っている場合にMatExprの型を自身で定義することは通常全くないと思います.)

InputArrayはMatExprを受けられるようにするかつ受けると同時にMatに変換してくれるようになるため,上記関数を以下のように演算結果を入力に使うことができるため利便性が増します.
例えば,Aの逆行列とXの転置,負のBを入力したかったら下記のように呼び出せばOKです.

void AXplusB2(A.inv(), X.t(), -B);

Matを入力とする関数では,上記のような使い方ではコンパイルエラーになるため,いちいち演算結果をMatで受けてから使う必要がありました.つまり,InputArrayを使うことで,呼び出す側でやっていたコーディングを関数側に書くことで,呼び出す側のコードを短く表現できるようになっています.

Mat Ainv=A.inv();
Mat Xtrans=X.t();
Mat Bm=-B;
void AXplusB1(Ainv, Xtrans, Bm);

2つ目:使う側が型を気にせず関数に値を入れることが出来る!

もうひとつのメリットは,入力がMatに限られず,様々な入力,例えば下記のものを任意に受け付ける関数が作れることです.
- Mat
- Mat_<T>: Mat_<uchar>, Mat_<float>, ...
- Matx
- vector: vector<uchar>, vector<float>, vector<cv::Point3f>, ...
- vector<vector<T>>
- vector<Mat>
- GpuMat (Cudaで計算するためのMat class)
- UMat(OpenCLと通常の関数を受ける共有クラス.詳細はこちら. http://www.slideshare.net/YasuhiroYoshimura/gpgpu-dandelion1124-201301130)

例えば,Nx3のfloatの行列とN個の配列となるvectorは,それぞれ保存している情報は同様であり,どちらも受け付ける関数にすると利便性が上がります.
また,UMatやGpuMatなどOpenCLやCUDAといった様々なデバイスで並列計算するためのクラスとMatを同時に受付て,利用者にはどちらから呼び出したか分からない透過的な利用をさせることも可能です.

これらの詳細は,次回のOpenCVの汎用入出力InputArray, OutputArrayの使い方(応用編)で説明します.

cv::OutputArrayの使い方

出力であるOutputArrayもInputArrayとほぼ同様ですが,一点だけ注意事項があります.それは,メモリの確保をOutputArrayの型を持つ名前で行う必要があることです.

具体的な例でどういう意味か示しましょう.これまでの行列の積和を行う関数を,出力を持つように下記のように拡張してみることを考えます.順当に出来ているように見えますが,これは正しく動作しません.

void AXplusB3(InputArray A_, InputArray X_, InputArray B_, OutputArray dest_)
{
    Mat A = A_.getMat();
    Mat X = X_.getMat();
    Mat B = B_.getMat();
    Mat dest = dest_.getMat();

    dest = A*X + B;
}

一方で,Matで入出力を作った関数を呼び出すと,当然,正しく出力されます.

void AXplusB4(Mat& A, Mat& X, Mat& B, Mat& dest)
{
    dest = A*X + B;
}

void inputArrayTest1()
{
    Mat a = Mat::ones(3,3,CV_32F);
    Mat x = Mat::ones(3, 1, CV_32F);
    Mat b = Mat::zeros(3, 1, CV_32F);
    Mat dest1, dest2;

    AXplusB3(a, x, b, dest1);
    AXplusB4(a, x, b, dest2);   
    cout << dest1 << endl;
    cout << dest2 << endl;
}

出力は下記のようになり,OutputArrayで作られた出力は空になっています.

[]
[3;
 3;
 3]

これは,参照カウントの仕組みによるもので,関数内で作られたMatは,関数のスコープを抜けるときに中身を開放するようになっています.つまり,せっかくの計算結果が失われてしまうようになっており,値渡しのときと似たような,C言語のポインタ渡しと値渡しの違いのような問題が発生しています.

これを解決するためには,OutputArray形式で内容を確保するか,Copyメソッドをよぶことで確保させる必要があります.つまり,正しいコードは下記のどちらかのようになります.

void AXplusB5(InputArray A_, InputArray X_, InputArray B_, OutputArray dest_)
{
    Mat A = A_.getMat();
    Mat X = X_.getMat();
    Mat B = B_.getMat();

    Mat dest = A*X + B;
    dest.copyTo(dest_);
}

出力の行列のサイズが分かっている場合は,OutputArrayを確保してから,getMat()メソッドを呼べばOKです.(今回の場合はXと同じサイズになるはず.)

void AXplusB6(InputArray A_, InputArray X_, InputArray B_, OutputArray dest_)
{
    dest_.create(X_.size(), X_.type());
    Mat A = A_.getMat();
    Mat X = X_.getMat();
    Mat B = B_.getMat();
    Mat dest = dest_.getMat();
    dest = A*X + B;
}

また,OutputArrayも&記号がついていませんが参照渡しです.下記のようにtypedefされているだけです.

typedef const _OutputArray& OutputArray;

なお,1.1くらいに出てきたスマートポインタを使ったPtrやその他マイナーなものは省略されました.ただし,スマートポインタのPtr自体は今も現役です.また,IplImageはおそらく入力を受け付けません.(ここ数年使ったことがないので試してみたことが自体がありません.)

まとめ

InputArrayとOutputArrayの基本的な使い方を説明しました.

27
27
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
27