はじめに
OpenCV.jpでは複数の画像を連結する方法が紹介されていますが、一つの画像の上に別の画像を重ねる方法(PiP:ピクチャ・イン・ピクチャ)は無いようです。
また、Stack OverflowではROI手法が紹介されていますが、領域サイズを間違えるとエラーになることや、元画像を変形して重ね合わせることができないなど、取り回しが悪いように思います。
そこで、本稿ではcv::warpAffineを使います。
行列を理解する手間がありますが、画像の変形に対応していることと、背景画像の外に描画してもエラーにならないなどの利点があります。
サンプルコード
アフィン変換
「アフィン変換」は画像を拡縮、回転、剪断、平行移動する処理です。
詳しくはOpenCV.jpのcv::warpAffine
を参照してください。
アフィン行列
アフィン変換は2行3列の行列で表されます。
a〜dが拡縮・回転・剪断を、tx, tyが平行移動を表します。
a | b | tx |
c | d | ty |
次のように記述すると任意のパラメータを指定できます。
cv::Mat mat = (cv::Mat_<double>(2,3)<<a, b, tx, c, d, ty);
より簡単にアフィン行列を作成する方法もあります。
回転行列はcv::getRotationMatrix2D
(サンプル2参照)
対応点からのアフィン行列はcv::getAffineTransform
(サンプル3参照)
アフィン行列の適用
背景画像の上にアフィン変形画像を重ねるには次のように記述します。
cv::warpAffine(smallImg, dstImg, mat, dstImg.size(), CV_INTER_LINEAR, cv::BORDER_TRANSPARENT);
cv::warpAffine
は第1引数が前景画像、第2引数が背景画像となっています。
第6引数にBORDER_TRANSPARENTを指定することで背景画像が保存されます(下図左)。第6引数を指定しないと前景画像以外が黒で塗りつぶされます(下図右)。
このBORDER_TRANSPARENTが本手法のキモです。
【左:第6引数にBORDER_TRANSPARENTを指定。右:第6引数未指定】
参考までに、第6引数の値と処理結果です
第6引数の値 | 背景画像の処理 |
---|---|
空欄 | 黒(または第7引数)で塗りつぶされる |
BORDER_TRANSPARENT | 背景画像を残して前景画像を重ねる |
BORDER_WRAP | 前景画像を繰り返す |
BORDER_REFLECT_101 | 前景画像の鏡像を繰り返す |
サンプル
サンプル1(平行移動)
画像を平行移動して貼り付けるサンプルです。
tx, tyが平行移動量。
この値が背景画像の範囲外になってもエラーは出ません。
void PinP_tr(const cv::Mat &srcImg, const cv::Mat &smallImg, const int tx, const int ty)
{
//背景画像の作成
cv::Mat dstImg;
srcImg.copyTo(dstImg);
//前景画像の変形行列
cv::Mat mat = (cv::Mat_<double>(2,3)<<1.0, 0.0, tx, 0.0, 1.0, ty);
//アフィン変換の実行
cv::warpAffine(smallImg, dstImg, mat, dstImg.size(), CV_INTER_LINEAR, cv::BORDER_TRANSPARENT);
imshow("affine", dstImg);
}
PinP_tr(srcImg, smallImg, 10, 10);
サンプル2(回転と平行移動)
画像を回転・平行移動して貼り付けるサンプルです。
angleが回転量(度)、tx, tyが平行移動量。
void PinP_rot_tr(const cv::Mat &srcImg, const cv::Mat &smallImg, const double angle, const double tx, const double ty)
{
//背景画像の作成
cv::Mat dstImg;
srcImg.copyTo(dstImg);
//前景画像の変形行列
cv::Point2d ctr(smallImg.cols/2, smallImg.rows/2);//前景画像の回転中心
cv::Mat mat = cv::getRotationMatrix2D(ctr, angle, 1.0);//回転行列の作成
mat.at<double>(0,2) +=tx;//回転後の平行移動量
mat.at<double>(1,2) +=ty;//回転後の平行移動量
//アフィン変換の実行
cv::warpAffine(smallImg, dstImg, mat, dstImg.size(), CV_INTER_LINEAR, cv::BORDER_TRANSPARENT);
imshow(winName, dstImg);
}
PinP_rot_tr(srcImg, smallImg, 45, 300, 100);
サンプル3 (貼り付け座標の指定)
前景画像の貼り付け先を座標で指定するサンプルです。
p0, p1が左上と右下の座標。
この2点で定義される長方形に前景画像を貼り付けます。
/**------------------------------------------------------------*
* @fn OpenCVのピクチャーインピクチャ
* @brief 画像内に画像を貼り付ける(位置を座標で指定)
* @par[in ] srcImg 背景画像
* @par[in ] smallImg 前景画像
* @par[in ] p0 前景画像の左上座標
* @par[in ] p1 前景画像の右下座標
*------------------------------------------------------------*/
void PinP_point(const cv::Mat &srcImg, const cv::Mat &smallImg, const cv::Point2f p0, const cv::Point2f p1)
{
//背景画像の作成
cv::Mat dstImg;
srcImg.copyTo(dstImg);
//3組の対応点を作成
vector<cv::Point2f> src, dst;
src.push_back(cv::Point2f(0, 0));
src.push_back(cv::Point2f(smallImg.cols, 0));
src.push_back(cv::Point2f(smallImg.cols, smallImg.rows));
dst.push_back(p0);
dst.push_back(cv::Point2f(p1.x, p0.y));
dst.push_back(p1);
//前景画像の変形行列
cv::Mat mat = cv::getAffineTransform(src, dst);
//アフィン変換の実行
cv::warpAffine(smallImg, dstImg, mat, dstImg.size(), CV_INTER_LINEAR, cv::BORDER_TRANSPARENT);
imshow(winName, dstImg);
}
//座標指定
cv::Point2f p0(100,100);
cv::Point2f p1(450,380);
PinP_point(srcImg, smallImg, p0, p1);