読者です 読者をやめる 読者になる 読者になる

OpenCV での画素値の安全な取得

OpenCV の開発者は非常に多くの機能を実装し提供してくれていますが,最も基本となる画素値への参照にはあまり関心がないようです.CV_IMAGE_ELEM や cvGet*D cvSet*D という機能は用意されていますが,使いにくくしかも安全ではありません.
というのも,画素へのアクセスで座標やチャンネル座標を間違えてしまうと領域外のアクセスになってしまうかもしれないからです(どこかの領域を破壊してしまうかもしれません).また,間違えた結果たまたま領域内の意図したのとは違う要素へのアクセスになってしまうかもしれません.さらに,画像の深度ビットに応じたキャストも必要になってきますが,実際にアクセスする画像の深度ビットに対応した型でキャストしなければなりません.
これは配列の添字と同じで,どんなに洗練されたプログラマが細心の注意を払っていても間違え得る問題です.
OpenCV は C で実装されているため,開発者は CV_IMAGE_ELEM をマクロで実装したかったのでしょう.そして,マクロで実装するがために assert のような機能をつけたくなかったのだろうと推測します(コードが長くなってしまうからです).そのため,CV_IMAGE_ELEM は実行時のエラーを何一つ検出することができません.さらに,C++ で開発を行っている方は CV_IMAGE_ELEM のようなマクロを使わないほうがよいでしょう.マクロは単純なテキスト置換であり,それ故さまざまな弊害があるからです.
cvGet*D/cvSet*D はマクロではなく関数で実装しているためか範囲チェックの assert は入っているようですが,型チェックまではしてくれません.さらに,cvGet*D/cvSet*D では関数呼び出しのオーバーヘッドが確実に加わります.個人的には,この種のオーバーヘッドは許容できる場合がほとんどかと考えますが,もしあなたが洗練されたプログラマであるのなら,ほんの少しの努力で取り除ける無駄は取り除きたいと思うでしょう.cvImageElem を使えばこのようなオーバーヘッドは生じません(assert による範囲/型チェックはリリースコンパイル時に消えて無くなり,関数呼び出しはインライン化されます).それと同時に,cvGet*D/cvSet*D を使い分ける煩わしさからも解放されます.

現在,CV_IMAGE_ELEM で画素値を取得されている方や,ご自身で画素値を取得する関数を実装し利用されている方で assert をご存じない方は,ご自身のコード中に上記のようなバグを埋め込んでしまっている可能性があります(今すぐ assert を追加しましょう).
中には assert と言われても何のことか分からない,という方もいらっしゃると思います.そのような方の為に,上記のエラーをすべて実行時に検出し,安全に画素値を参照する関数を紹介します(コンパイル時に検出できないのが残念なところです).

ここで,予備知識として以下のリンク先で記述されている OpenCV の画素値の格納方法を用います.

単にピクセル値を取得したい方はわざわざ上記リンクを読み格納方法を理解する必要はなく,以下に実装しました cvImageElem を用いればよいでしょう(用いるべきです).
以上のリンク先の予備知識を用いて,画素値を安全に参照する関数を実装したソースコードを以下に示します(cvie.hpp を include するだけで使えます).

cvie.hpp

#ifndef CVIE_HPP_20081011
#define CVIE_HPP_20081011

#if defined(_MSC_VER) && (_MSC_VER >= 1020)
# pragma once
#endif


#include <cassert>
#include <cxtypes.h>


namespace detail {
    template <int> struct CvImageElemTypeTraits;
    template <>    struct CvImageElemTypeTraits<IPL_DEPTH_1U>  { typedef bool           ImageElemType; };
    template <>    struct CvImageElemTypeTraits<IPL_DEPTH_8U>  { typedef unsigned char  ImageElemType; };
    template <>    struct CvImageElemTypeTraits<IPL_DEPTH_8S>  { typedef char           ImageElemType; };
    template <>    struct CvImageElemTypeTraits<IPL_DEPTH_16U> { typedef unsigned short ImageElemType; };
    template <>    struct CvImageElemTypeTraits<IPL_DEPTH_16S> { typedef short          ImageElemType; };
    template <>    struct CvImageElemTypeTraits<IPL_DEPTH_32S> { typedef int            ImageElemType; };
    template <>    struct CvImageElemTypeTraits<IPL_DEPTH_32F> { typedef float          ImageElemType; };
    template <>    struct CvImageElemTypeTraits<IPL_DEPTH_64F> { typedef double         ImageElemType; };
}


template <int IPL_DEPTH>
inline typename detail::CvImageElemTypeTraits<IPL_DEPTH>::ImageElemType&
cvImageElem(const IplImage* cvImage, int x, int y, int channel)
{
    assert((0 <= x) && (x < cvImage->width) && "cvImageElem(): x座標が範囲外です");
    assert((0 <= y) && (y < cvImage->height) && "cvImageElem(): y座標が範囲外です");
    assert((0 <= channel) && (channel < cvImage->nChannels) && "cvImageElem(): channelが範囲外です");
    assert(IPL_DEPTH == cvImage->depth && "cvImageElem(): 型が一致しません");

    const int channels = cvImage->nChannels;

    typedef typename detail::CvImageElemTypeTraits<IPL_DEPTH>::ImageElemType ImageElemType;

    return reinterpret_cast<ImageElemType*>(cvImage->imageData + cvImage->widthStep * y)[x * channels + (channels - 1 - channel)];
}


#endif

cvie.hpp に画素値にアクセスできる関数 cvImageElem を実装しました.cvie.hpp を include するだけで cvImageElem が使用可能になります.
cvImageElem のテンプレート引数に,アクセスしたい画像の深度ビットを表す定数を指定して使います(詳しくはテストコードを見てください).デバッグ時には範囲チェックを行い,不正なアクセスがないようにしています.
以下のテストコードで cvImageElem の動作を確認できます.

test.cpp

#include "cvie.hpp"
#include <cxcore.h>
#include <highgui.h>


#ifdef _MSC_VER
# pragma comment(lib, "cxcore.lib")
# pragma comment(lib, "highgui.lib")
#endif


int main()
{
    CvImage image1(cvCreateImage(cvSize(512, 512), IPL_DEPTH_8U, 3));
    CvImage image2(cvCreateImage(cvSize(512, 512), IPL_DEPTH_64F, 2));

    for (int y = 0; y < 512; ++y) {
        for (int x = 0; x < 512; ++x) {
            // 値の取得は次のようにします
            // 関数 cvImageElem<IPL_DEPTH_8U>() の返り値は unsigned char になります.
            const unsigned char r = cvImageElem<IPL_DEPTH_8U>(image1, x, y, 0); // 0番目のチャンネル,つまり赤
            const unsigned char g = cvImageElem<IPL_DEPTH_8U>(image1, x, y, 1); // 1番目のチャンネル,つまり緑
            const unsigned char b = cvImageElem<IPL_DEPTH_8U>(image1, x, y, 2); // 2番目のチャンネル,つまり青

            // 関数 cvImageElem<IPL_DEPTH_64F>() の返り値は double になります.
            const double q = cvImageElem<IPL_DEPTH_64F>(image2, x, y, 0);
            const double w = cvImageElem<IPL_DEPTH_64F>(image2, x, y, 1);

            // 値の代入も同じようにできます
            cvImageElem<IPL_DEPTH_8U>(image1, x, y, 0) = 255;
            cvImageElem<IPL_DEPTH_8U>(image1, x, y, 1) = 255;
            cvImageElem<IPL_DEPTH_8U>(image1, x, y, 2) = 255;

            cvImageElem<IPL_DEPTH_64F>(image2, x, y, 0) = 3.14 * x * y;
            cvImageElem<IPL_DEPTH_64F>(image2, x, y, 1) = (2.71 * x) / y;

            // 次のようにピクセルへのポインタを取得することもできます
            unsigned char* p = &cvImageElem<IPL_DEPTH_8U>(image1, x, y, 2); // BGR の順に格納されていることに注意

            *(p + 0) = 0;
            *(p + 1) = 0;
            *(p + 2) = 0;

            // 次のような範囲外のアクセスは実行時エラーとして検知します

            // チャンネル座標を間違えています
            // cvImageElem<IPL_DEPTH_8U>(image1, x, y, 3);

            // x座標が有効な範囲を超えてしまいます
            // cvImageElem<IPL_DEPTH_64F>(image2, x + 1, y, 0);

            // image2 の画素を IPL_DEPTH_8U としてアクセスしています(image2 の画素は実際には IPL_DEPTH_64F です)
            // cvImageElem<IPL_DEPTH_8U>(image2, x, y, 0);
        }
    }

    return 0;
}
  • VC9
  • g++ (GCC) 3.4.4 (cygming special)

でのコンパイルを確認しています(CではなくC++コンパイルする必要があります).
cvie.hpp はここからダウンロードできます.


次回はLU分解について紹介しようと思います.
それでは.