2015年7月5日日曜日

cv::Point_クラスの落とし穴?

OpenCVのcv::Point_クラスを積極的に使うと、添字の順序を間違える危険性も少なくなりコードが見やすくなる。 でも、OpenCVのマニュアルは読んでもソースコードまでは読まないような 私のような初心者にとってはなかなか気づきにくい落とし穴を発見した。(OpenCV version 3.0.0)

落とし穴の例

次のコードは12行目の「src(point2d)」の部分で、 行列srcの添字が範囲外となって実行時エラーになる可能性がある。 func(j, i)は、整数の座標(i, j)を入れたら浮動小数点の座標posを返す関数で 画像の回転など幾何変換などで頻繁に出くわす状況だと思う。 「posが行列の範囲外になってしまう可能せがあるからだ」、 と思いきやif文でしっかり範囲指定されていて問題ないかのように見える。

cv::Mat_<uchar> src(480, 640), dst(480, 640);

for (int j = 0; j < color.rows; j++)
{
    for (int i = 0; i < color.cols; i++)
    {
        cv::Point2d pos = func(j, i);

        if (0 <= pos.x && pos.x < src.cols &&
            0 <= pos.y && pos.y < src.rows)
        {
            dst(j, i) = src(pos);
        }
    }
}

原因はdoubleからintへの暗黙の型変換

実はこれposがcv::Point2dでdouble型、つまり浮動小数点であることが原因。 というよりOpenCVの仕様をよく知らずに上記の様に書いたことが本質的な原因だけど。 この原因を理解するために、srcのクラスcv::Mat_<uchar>のメンバ関数operator()()をを確認してみる。 というのもoperator()()は行列の要素にアクセスする関数なので、 行列の添字として整数しか受け付けないはずなので。

template<typename _Tp> inline
_Tp& Mat_<_Tp>::operator ()(Point pt)
{

引数としてPointクラスしか受け付けないことが分かったが、 Pointクラスってなんぞやと思いさらに定義を確認。

typedef Point_<int> Point2i;
(中略)
typedef Point2i Point;

Pointクラス=Point2iクラスであることが分かり、 Point2i=Point_<int>クラスであることが分かった。 問題のコードのコンパイルが通っていたということは、 Point2d、つまりPoint_<double>クラスから Point_<int>クラスに暗黙的に型変換されていたことが分かる。 では、その暗黙の型変換に使われたキャスト用の関数があるはずなので、次はこれも確認する。

saturate_castとは四捨五入する型変換

//! conversion to another data type
template<typename _Tp> template<typename _Tp2> inline
Point_<_Tp>::operator Point_<_Tp2>() const
{
    return Point_<_Tp2>(saturate_cast<_Tp2>(x), saturate_cast<_Tp2>(y));
}

これがその型変換の関数。 分かりにくいけど、これは同じテンプレートクラスPoint_の型だけが違うものどうしの変換を扱う関数。 つまり、ここでは_Tp2がint型で、_Tpがdouble型ということになり、 Pint_<double>::operator Point_<int>()という型変換関数だと思えばいい。 テンプレートを用いた型変換に、こんな書き方があること自体知らなかった。 単にPint_<_Tp>::Point_<_Tp>(const Pint_<_Tp2>& p)などと書いてしまいそう。

とにかく、その型変換用のoperator()()の中ではsaturate_castt<int>(double v)というのが呼ばれて、 double型で座標を入れてもちゃんとint型に変換されるようだ。 では、そのsaturate_castって何か?

template<> inline int saturate_cast<int>(double v)           { return cvRound(v); }

なんとこの型変換関数の中では四捨五入の関数cvRound()が呼ばれてる。 なるほどそれで問題の本当の原因が分かった!! つまり、問題のコードの中でif文中で範囲を限定してもsrc(pos)範囲外になる場合がある。

反例は、pos.xの値が639.9などの時。 pos.x < src.colsが真であったとしても、 src(pos)では最終的にcvRound(pos.x)が呼ばれて、640になってしまう。 srcの列数は640なのでもちろん添字は639が最大だからエラーになったという訳。

エラーの回避策

エラーの回避策の一つはif文で判定する前に整数に変換されるようにすること。 つまり次のように座標変換関数の出力が浮動小数点であっても、 整数の座標クラスで受けて整数に変換してしまえば、 if文の判定とsrc(pos)の範囲がズレることはない。

cv::Point pos = func(j, i);