#はじめに
これは、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画像をグレースケール画像に変換する
利用できるパラメータは以下のように定義されています (一例を抜粋)
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
で始まる。(例:cvCreateImage
、cvCvtColor
) - OpenCV 由来の構造体は
Cv
で始まる。(例:CvMat
、CvPoint2D
) - OpenCV 由来の定数は
CV_
で始まる。(例:CV_RGB2Gray
、CV_INTER_NN
)
#関数も構造体もcvが外されたのに、定数ときたら
一方でC++には、名前空間という仕組みがあります。基本的にOpenCV のすべての関数、クラス、構造体、定数などは cv
名前空間に属します。これによりもし他のライブラリで同じ名前の関数があったとしても、衝突を回避できるわけです。
OpenCV 3.0が出て早6ヶ月、OpenCV 2.2が登場して早5年が経過しました。Web上ではC言語の関数、IplImageやCvMatをはじめとするC言語の構造体の記述は、過去の遺物であり、新規の情報源としては上がってこなくなった気がします。しかし、CV_
で始まる定数群はまだまだ現役で使われている感じがあります。
現に、OpenCV の内部実装でも、未だCV_
で始まる定数が大量に使われています。(例:cvtColor
内部)
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_8U
やCV_32F
自体は0-6までの数字が振られているので、enumとして定義することは可能です。しかし、実際に定数をMat内に情報として保存する場合、bit深度以外にチャンネル数、疎行列か否かといった情報と共に、符号化して保存されます。なのでenumで定義するよりdefineで定義した方が何かと便利なのです。
#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列に縮退することができます。ここでは単純にグレースケールの画像を平均値を求めることで縮退し、また画像自体を自乗することで、自乗の平均も計算します。最後に自乗の平均から平均の自乗を引くことで分散を求めます。
ちなみに求めた分散が以下の図です
右側から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に正規化してるので、全体的に黒く見える訳です。
一方で、4チャンネル画像として保存したLenaさんは、なかなか芸術的な幾何学模様になっています。
これは単純な整数型でなく、float型として保存しているので、符号化されたbitが全体的に砂嵐の様に現れるわけです。
ただし、MSB (最上位bit) は符号bitであり、これはpng画像のうち、アルファチャンネルのMSBでもあります。なので、砂嵐と同時に全体的に白黒の2値画像の雰囲気も付いているのは、これが原因であります。
一件、壊れたように見えるfloat画像ですが、これを再度読み込んで0.0f--1.0fで正規化すると、以下のようにもとのLenaさんに戻せます。
#まとめ
未だ使われ続けるCV_
で始まる~~定数群を駆逐したくて、~~定数群の代替を紹介しました。
どうしてもCV_8U
やCV_32F
を駆逐できなかったのが心残りです。
#備考
筆者は以下の環境で動作確認しました.
- OpenCV 3.0.0
- Windows 7 Professional (64bit)
- Visual Studio 2012 Update4
#補足事項
この記事はあくまで手島個人が CV_
で始まる定数が好きではない、cv空間内のenumの方が好きだ!という主旨で書いている記事です。CV_
で始まる定数を使ってるWeb上の記事を非難しているわけではありません。同値であるので、cv::INTER_NEAREST
も CV_INTER_NN
も、好きな方を使えば良いと思います。