9
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

OpenCVAdvent Calendar 2015

Day 16

C時代の enum CV_** を駆逐したい

Last updated at Posted at 2015-12-15

#はじめに
これは、OpenCV Advent Calendar 2015 16日目の記事です。関連記事は目次にまとめられています。

OpenCV のC++サポートが入ったのは2.0からで、1.0系列およびその昔はC言語がメインなサポート対象でしたが、OpenCV 3.0 からはC++がサポートされ、C言語はサポート対象外へと変わっています。
OpenCV 2.0 はまさにその過渡期で、Cインタフェースしか無い関数とか、C++インタフェースはあるけれど実体はC版のラッパー(またはその逆)とか、かなり混在していました。今でもその名残は多々存在します。
その名残の一つとして、未だ使われているCV_で始まる定数シンボルがあります。

CV_で始まる定数シンボルを、一匹残らず駆逐してやる!

#C時代の名残とは
OpenCV では、関数内部の挙動を変化させるために、引数に定数シンボルを渡すことがあります。例えば、cvtColor がその代表例です。

void cvtColor( InputArray src, OutputArray dst, int code)

この色変換を行う関数のうち、code引数で適切なパラメータを渡すことで、望み通りの色変換を行うことができます。下記例だと、CV_BGR2GRAY がそれに該当します

    using namespace cv;
    Mat color = imread("color.png");
    Mat gray;
    cvtColor(color, gray, CV_BGR2GRAY); // CV_BGR2GRAY はRGB画像をグレースケール画像に変換する

利用できるパラメータは以下のように定義されています (一例を抜粋)

types_c.h
 enum
{
    CV_BGR2BGRA    =0,
    CV_RGB2RGBA    =CV_BGR2BGRA,

    CV_BGRA2BGR    =1,
    CV_RGBA2RGB    =CV_BGRA2BGR,
    // 一部中略
    CV_BayerBG2RGB_EA = CV_BayerRG2BGR_EA,
    CV_BayerGB2RGB_EA = CV_BayerGR2BGR_EA,
    CV_BayerRG2RGB_EA = CV_BayerBG2BGR_EA,
    CV_BayerGR2RGB_EA = CV_BayerGB2BGR_EA,

    CV_COLORCVT_MAX  = 139
};

このCV_で始まる定数(enum)は、OpenCV のC時代の名残であったりします。
1.0系列時代のOpenCV では、OpenCV 由来の関数/定数/構造体を示すために、以下のような法則がありました。

  • OpenCV 由来の関数は cv で始まる。(例:cvCreateImagecvCvtColor
  • OpenCV 由来の構造体は Cv で始まる。(例:CvMatCvPoint2D
  • OpenCV 由来の定数は CV_ で始まる。(例:CV_RGB2GrayCV_INTER_NN

#関数も構造体もcvが外されたのに、定数ときたら
一方でC++には、名前空間という仕組みがあります。基本的にOpenCV のすべての関数、クラス、構造体、定数などは cv 名前空間に属します。これによりもし他のライブラリで同じ名前の関数があったとしても、衝突を回避できるわけです。

OpenCV 3.0が出て早6ヶ月、OpenCV 2.2が登場して早5年が経過しました。Web上ではC言語の関数、IplImageやCvMatをはじめとするC言語の構造体の記述は、過去の遺物であり、新規の情報源としては上がってこなくなった気がします。しかし、CV_で始まる定数群はまだまだ現役で使われている感じがあります。

現に、OpenCV の内部実装でも、未だCV_で始まる定数が大量に使われています。(例:cvtColor内部)

color.cpp
void cv::cvtColor( InputArray _src, OutputArray _dst, int code, int dcn )
{
    int stype = _src.type();
    int scn = CV_MAT_CN(stype), depth = CV_MAT_DEPTH(stype), bidx;

    // 一部略
    switch( code )
    {
        case CV_BGR2BGRA: case CV_RGB2BGRA: case CV_BGRA2BGR:
        case CV_RGBA2BGR: case CV_RGB2BGR: case CV_BGRA2RGBA:
    // 一部略
            break;
        case CV_BGR5652BGR: case CV_BGR5552BGR: case CV_BGR5652RGB: case CV_BGR5552RGB:
        case CV_BGR5652BGRA: case CV_BGR5552BGRA: case CV_BGR5652RGBA: case CV_BGR5552RGBA:
    // 一部略
            break;
        case CV_BGR2YCrCb: case CV_RGB2YCrCb:
        case CV_BGR2YUV: case CV_RGB2YUV:
    // 一部略
            break;
    // 一部略
        default:
            CV_Error( CV_StsBadFlag, "Unknown/unsupported color conversion code" );
    }

CV_ で始まる定数だけが残っている現状がなんとなく気持ち悪いので、同値の enum であり、かつ cv 名前空間内で宣言されているものを紹介します。ほとんどが、enumでシリーズとして定義されているので、各シリーズのうち、1つずつを紹介しています。全部は網羅してないと思いますが、あくまで一部を紹介します。

#同値のenum達
##calib3dモジュール内

CV_で始まる定数 cv名前空間内
CV_LMEDS LMEDS
CV_ITERATIVE SOLVEPNP_ITERATIVE
CV_CALIB_CB_ADAPTIVE_THRESH CALIB_CB_ADAPTIVE_THRESH
CV_CALIB_USE_INTRINSIC_GUESS CALIB_USE_INTRINSIC_GUESS
CV_FM_7POINT FM_7POINT

対応点関係やF行列を推定するための手法、また最適化手法などを選択します。RANSACはいいね。RANSACは心を潤してくれる。リリンの生み出した文化の極みだよ。

##coreモジュール内

CV_で始まる定数 cv名前空間内
CV_REDUCE_SUM REDUCE_SUM
CV_L1 NORM_L1
CV_COVAR_SCRAMBLED COVAR_SCRAMBLED
CV_LU DECOMP_LU
CV_CMP_EQ CMP_EQ
CV_DXT_INVERSE DFT_INVERSE
CV_LUのように、もともと短い定数はDECOMP_LUの様に、修飾する単語が増えたりします。
後述するREDUCE_MAXは、このモジュール内で定義されています。

highguiモジュール内

CV_で始まる定数 cv名前空間内
CV_FONT_LIGHT QT_FONT_LIGHT
CV_STYLE_NORMAL QT_STYLE_NORMAL
CV_WND_PROP_AUTOSIZE WND_PROP_AUTOSIZE
CV_WINDOW_AUTOSIZE WINDOW_AUTOSIZE
CV_WINDOW_FULLSCREEN WINDOW_FULLSCREEN
CV_EVENT_MOUSEMOVE EVENT_MOUSEMOVE
CV_EVENT_FLAG_LBUTTON EVENT_FLAG_LBUTTON
単純にCV_を取り除くだけでほとんどの定数が置き換え可能です。highguiと言えば3.0より前は画像の読み書き、ウィンドウでの表示など、基本的なところでお世話になってますが、OpenCV 3.0からはあくまでGUI関連がhighgui、画像ファイルの読み書きはimgcodecsモジュールが担当となっています。

##imgcodecsモジュール内

CV_で始まる定数 cv名前空間内
CV_LOAD_IMAGE_UNCHANGED IMREAD_UNCHANGED
CV_IMWRITE_JPEG_QUALITY IMWRITE_JPEG_QUALITY
imgcodecsは昔でいうhighguiのファイル読み書き担当です。OpenCV 3.0からは別モジュールに分けられました。
OpenCV ではファイルを保存する際に、ファイルごとにオプションを指定できます。
ここに出てるIMWRITE_JPEG_QUALITYはjpg形式で画像を保存する際にファイルサイズとノイズのトレードオフを選ぶためのパラメータです。

##imgprocモジュール内

CV_で始まる定数 cv名前空間内
CV_SHAPE_RECT MORPH_RECT
CV_CLOCKWISE true/falseに変更
CV_COMP_CORREL HISTCMP_CORREL
CV_DIST_MASK_3 DIST_MASK_3
CV_DIST_LABEL_CCOMP DIST_LABEL_CCOMP
CV_DIST_L1 DIST_L1
CV_THRESH_BINARY THRESH_BINARY
CV_ADAPTIVE_THRESH_MEAN_C ADAPTIVE_THRESH_MEAN_C
CV_FLOODFILL_FIXED_RANGE FLOODFILL_FIXED_RANGE
CV_HOUGH_STANDARD HOUGH_STANDARD
CV_BGR2BGRA COLOR_BGR2BGRA
CV_INTER_NN INTER_NEAREST
CV_WARP_FILL_OUTLIERS WARP_FILL_OUTLIERS
CV_MOP_ERODE MORPH_ERODE
CV_TM_SQDIFF TM_SQDIFF
CV_RETR_EXTERNAL RETR_EXTERNAL
CV_CHAIN_CODE CHAIN_APPROX_NONE
CV_AA1 LINE_AA
大量にあるCOLOR_BGR2BGRAの他にも、INTER_NEAREST(補間方法)を表すenumがたくさんあります。

##objdetectモジュール内

CV_で始まる定数 cv名前空間内
CV_HAAR_DO_CANNY_PRUNING CASCADE_DO_CANNY_PRUNING
Haar特徴量の内部で使うフラグです。詳細は未調査

##photoモジュール内

CV_で始まる定数 cv名前空間内
CV_INPAINT_NS INPAINT_NS
CV_INPAINT_TELEA INPAINT_TELEA
OpenCV 3.0から登場したphotoモジュールです。詳細は未調査

##videoモジュール内
幾つかあるものの、対応関係が見つからず

##videoioモジュール内

CV_で始まる定数 cv名前空間内
CV_CAP_ANY CAP_ANY
CV_CAP_PROP_DC1394_OFF CAP_PROP_POS_MSECPROP
CV_CAP_MODE_BGR CAP_MODE_BGR
CV_CAP_OPENNI_DEPTH_MAP CAP_OPENNI_DEPTH_MAP
CV_CAP_OPENNI_VGA_30HZ CAP_OPENNI_VGA_30HZ
CV_CAP_INTELPERC_DEPTH_MAP CAP_INTELPERC_DEPTH_MAP
CV_CAP_PROP_GPHOTO2_PREVIEW CAP_PROP_GPHOTO2_PREVIEW

CAP_PROP_POS_MSECPROPを始めとするプロパティ達は、キャプチャライブラリごとに細分化されています
もともと、全キャプチャライブラリ用の定数が全部数珠つなぎに同じenumになってましたが、
細分化されたことで、ヘッダを眺めた時に見やすくなるんじゃないか、と思ってます。

#例外
調べた限り、すべてのCV_定数が置換可能なわけではなく、例外があります。そして、最も使われると言っても過言ではない定数がその例外だったりします。

  • CV_8U
  • CV_8S
  • CV_16U
  • CV_16S
  • CV_32S
  • CV_32F
  • CV_64F

こいつらは、Matクラスの、各要素のbit深度を表す定数です。
OpenCV のソースコードを眺めてみましたが、こいつらだけは未だcvdef.h内で定義されたdefineしか存在せず、代替の定数はどこにも定義されていません。 (見落としてるだけだったらこっそり手島まで教えて下さい)

こいつらが、リプレースされないのは、面倒くさい 誰も着手してない 誰も不便を感じてない 忙しくて後回しになっている可能性もありますが、思うに以下2点がネックになって居るのではないかと思います。

  • CV_8Uなどの定数はチャンネル数とともに符号化される
  • 先頭のCV_を外すと、数字が先頭になるので、文字列として利用できない

##CV_8Uなどの定数はチャンネル数とともに符号化される

CV_8UCV_32F自体は0-6までの数字が振られているので、enumとして定義することは可能です。しかし、実際に定数をMat内に情報として保存する場合、bit深度以外にチャンネル数、疎行列か否かといった情報と共に、符号化して保存されます。なのでenumで定義するよりdefineで定義した方が何かと便利なのです。

cvdefs.h
#define CV_8U   0
#define CV_8S   1
#define CV_16U  2
#define CV_16S  3
#define CV_32S  4
#define CV_32F  5
#define CV_64F  6
#define CV_USRTYPE1 7

#define CV_MAT_DEPTH_MASK       (CV_DEPTH_MAX - 1)
#define CV_MAT_DEPTH(flags)     ((flags) & CV_MAT_DEPTH_MASK)

#define CV_MAKETYPE(depth,cn) (CV_MAT_DEPTH(depth) + (((cn)-1) << CV_CN_SHIFT))  // このdefineを使ってbit深度(depth)とチャンネル数(cn)を符号化する
#define CV_MAKE_TYPE CV_MAKETYPE

#define CV_8UC1 CV_MAKETYPE(CV_8U,1)
#define CV_8UC2 CV_MAKETYPE(CV_8U,2)
#define CV_8UC3 CV_MAKETYPE(CV_8U,3)
#define CV_8UC4 CV_MAKETYPE(CV_8U,4)
#define CV_8UC(n) CV_MAKETYPE(CV_8U,(n))

##先頭のCV_を外すと、数字が先頭になるので、文字列として利用できない
CV_8Uから、8Uとなってしまい、定数として定義できません。イマイチいいネーミングが思い浮かばずに、変更できずに居るのかも?でも、よく考えればDEPTH_8Uとか使えば、行けそうな気もします。おそらく符号化が原因でdefineの方が都合が良いので、enumにしようという方向に動かないのでしょう。

#サンプルコード
ただ定数を紹介しただけではワクワクがありませんね。dandelion1124先生に煽られた2から、という訳では決してありませんが、サンプルコードを以下に書いておきます。
##cv::reduceを使った例
みんな大好きLenaさんの列ごとの分散を求めてみましょう。皆さんもこういうコード書きたいことってありませんか?私はよくあります.

    using namespace cv;
    cv::Mat rgb = imread("lena.png");
    
    // カラー画像をグレースケール画像に変換
    Mat gray;
    cvtColor(rgb, gray, COLOR_BGR2GRAY); // CV_BGR2GRAYじゃないよ!
    gray.clone().convertTo(gray, CV_32F);
    
    // 平均の計算
    Mat average;
    reduce(gray, average, 0, REDUCE_AVG); // CV_REDUCE_AVGじゃないよ!
    
    // 自乗の平均の計算
    Mat square;
    Mat variance;
    square = gray.mul(gray);
    average = average.mul(average);
    reduce(square, variance, 0, REDUCE_AVG); // CV_REDUCE_AVGじゃないよ!
    
    // 自乗の平均から平均の自乗を引くと分散になる
    variance -= average;
    
    // 表示用にスケーリングする
    double min, max;
    minMaxLoc(variance, &min, &max);
    variance /= max;
    resize(variance.clone(), variance, Size(variance.cols, outputImageHeight), INTER_NEAREST); // CV_INTER_NN じゃないよ!
    
    // 表示する
    namedWindow(outputWindowName, WINDOW_FREERATIO);  // CV_WINDOW_FREERATIOじゃないよ!
    imshow(outputWindowName, variance);
    
    // 保存用に再度スケーリングする
    variance *= whiteValue;
    imwrite("lena_statistics.png", variance);

みんな大好きcv::reduceで、画像を1行、もしくは1列に縮退することができます。ここでは単純にグレースケールの画像を平均値を求めることで縮退し、また画像自体を自乗することで、自乗の平均も計算します。最後に自乗の平均から平均の自乗を引くことで分散を求めます。

ちなみに求めた分散が以下の図です
lena_statistics.png
右側から1/4ぐらいのところが分散が大きいことがわかります。Lenaさんの黒い髪の毛、白い肩が同じ列に存在するので、分散が大きくなってるわけですね。

##cv::imwrite/cv::imreadを使った例
もう1例、imwrite/imreadを使ってfloatの値をそのままファイルに保存します。
OpenCV では、基本的にpngやjpegなど、画素それぞれは整数値を持ちます。単純にfloatを保存しようとすると、うまく行きません。
画像のfloatの値をそのままファイルに保存してみましょう。皆さんもこういうコード書きたいことってありませんか?私はよくあります.

    using namespace cv;
    Mat floatImage;
    Mat lena = imread("lena.png", IMREAD_GRAYSCALE); // CV_LOAD_IMAGE_GRAYSCALE じゃないよ!
    
    // floatに無理やり変換
    // この時点で4byte / pixelのシングルチャンネル画像ができる
    lena.convertTo(floatImage, CV_32F); // く、駆逐できない
    
    // floatらしさのために、-1.0 -- 1.0 の間で正規化する
    double scaleMax, scaleMin;
    minMaxLoc(floatImage, &scaleMin, &scaleMax);
    floatImage /= (scaleMax - scaleMin);
    floatImage *= 2.0f;
    floatImage -= 1.0f;
    
    namedWindow(outputWindowName);
    imshow(outputWindowName, floatImage); // 途中経過を表示
    waitKey(0);
    
    // 無理やり4チャンネルの8bit画像に変換する
    // Matの第4引数にポインタを渡すことで、データを保持したまま、別の画像から参照することができる
    Mat writeImage = Mat(floatImage.rows, floatImage.cols, CV_8UC4, floatImage.data); // く、駆逐できない
    
    // png は4 channel 受け付ける
    // bmp も保存可能
    imwrite("float.png", writeImage);
    
    // 再度読み込む
    Mat reload = imread("float.png", IMREAD_UNCHANGED); // CV_LOAD_IMAGE_UNCHANGEDじゃないよ!
    // 上記の逆変換を行い、32bitシングルチャンネルに戻す
    Mat showImage = Mat(reload.rows, reload.cols, CV_32FC1, reload.data); // く、駆逐できない
    
    // 今度は 0.0 -- 1.0 の間で正規化する
    showImage += 1.0f;
    showImage /= 2.0f;
    
    imshow(outputWindowName, showImage);
    waitKey(0);

このように、floatの画像として保存するのでなく、8bit 4チャンネル(32bit)の画像として保存することで無理やり保存できる
途中経過時点の画像が以下のような図です。全体的に暗い画像になっているのは、OpenCV のウィンドウでfloatを表示する場合は0.0f-1.0fの間しか表示できず、それ以下は黒に、それ以上は白に丸め込まれます。
直前で-1.0f--1.0に正規化してるので、全体的に黒く見える訳です。

lena_float.png

一方で、4チャンネル画像として保存したLenaさんは、なかなか芸術的な幾何学模様になっています。

float.png

これは単純な整数型でなく、float型として保存しているので、符号化されたbitが全体的に砂嵐の様に現れるわけです。
ただし、MSB (最上位bit) は符号bitであり、これはpng画像のうち、アルファチャンネルのMSBでもあります。なので、砂嵐と同時に全体的に白黒の2値画像の雰囲気も付いているのは、これが原因であります。

一件、壊れたように見えるfloat画像ですが、これを再度読み込んで0.0f--1.0fで正規化すると、以下のようにもとのLenaさんに戻せます。

lena_reconstruct.png

#まとめ
未だ使われ続けるCV_で始まる~~定数群を駆逐したくて、~~定数群の代替を紹介しました。
どうしてもCV_8UCV_32Fを駆逐できなかったのが心残りです。

#備考
筆者は以下の環境で動作確認しました.

  • OpenCV 3.0.0
  • Windows 7 Professional (64bit)
  • Visual Studio 2012 Update4

#補足事項
この記事はあくまで手島個人が CV_ で始まる定数が好きではない、cv空間内のenumの方が好きだ!という主旨で書いている記事です。CV_で始まる定数を使ってるWeb上の記事を非難しているわけではありません。同値であるので、cv::INTER_NEARESTCV_INTER_NN も、好きな方を使えば良いと思います。

  1. UnaNancyOwen先生にご指摘頂きましたので、追加します。

  2. dandelionさんはTwitterを使っています: "@tomoaki_teshima テクい記事楽しみにしてます!"

9
9
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
9
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?