C++ Opencv 画像を合成する

最初に

今回は下の3つの画像を用いて、差分処理を行って画像を合成します。
青空の画像が背景で、Opencvのロゴが前景、真っ黒な画像がロゴを抜き取るための画像になります。

f:id:kisaragi211:20191102192834j:plainf:id:kisaragi211:20191102192829j:plainf:id:kisaragi211:20191102192901j:plain

ソース

#include <iostream>
#include <string>
#include <sstream>
#include <iomanip>
#include <opencv2/opencv.hpp>

/* img1とimg2の差分を返す */
cv::Mat frame_sub(cv::Mat& img1, cv::Mat& img2, int th = 20) {

  cv::Mat diff;
  cv::Mat mask;

  cv::absdiff(img1, img2, diff);
  cv::threshold(diff, mask, th, 255, cv::THRESH_BINARY);

  return mask;

}

/* 画像を合成する */
int image_composition(std::string base_path, std::string fg_path, std::string bg_path, std::string result_path, int x_pos = 0, int y_pos = 0, int th = 10) {

  // ベース画像の設定
  cv::Mat base_frame;
  base_frame = cv::imread(base_path.c_str());
  cv::cvtColor(base_frame, base_frame, cv::COLOR_BGR2GRAY);

  // 画像の読み込み 
  cv::Mat bg = cv::imread(bg_path.c_str());
  cv::Mat fg = cv::imread(fg_path.c_str());

  // ROIがbg画像内に収まるように座標を調整
  if (x_pos + fg.cols > bg.cols) {
    x_pos = bg.cols - fg.cols;
  }
  if (y_pos + fg.rows > bg.rows) {
    y_pos = bg.rows - fg.rows;
  }
  if (x_pos < 0 || y_pos < 0)
    return -1;

  // ROI(領域)の作成 
  cv::Rect rect_roi(x_pos, y_pos, fg.cols, fg.rows);
  cv::Mat roi = bg(rect_roi);

  cv::Mat fg_gray;
  cv::cvtColor(fg, fg_gray, cv::COLOR_BGR2GRAY);

  cv::Mat mask;    
  mask = frame_sub(base_frame, fg_gray, th);

  cv::Mat mask_inv;
  cv::bitwise_not(mask, mask_inv);  

  cv::Mat fg_down;
  cv::bitwise_and(roi, roi, fg_down, mask_inv);  

  cv::add(fg, fg_down, roi);

  cv::imwrite(result_path.c_str(), bg);
  

  return 0;

}

コード解説

今回二つの関数があるので、それぞれについて書いていきます。

frame_sub解説

関数が二つありますが、まずは上の処理のframe_subについて書きます。

cv::Mat frame_sub(cv::Mat& img1, cv::Mat& img2, int th = 20) {

  cv::Mat diff;
  cv::Mat mask;

  cv::absdiff(img1, img2, diff);
  cv::threshold(diff, mask, th, 255, cv::THRESH_BINARY);

  return mask;
}

ここはimag1とimg2の画像の差分を返します。なので、今回はOpencvのロゴ画像を真っ黒画像の差分を得るために使います。
absdiffが実際に差分を取り出しているところです。取り出した差分がこれです。
f:id:kisaragi211:20191102202600j:plain:w300:h300
これをmaskとして返したいのですが、このままだと色に違いがありマスクとして使えないのでさらに手を加えます。
cv::threshold(diff, mask, th, 255, cv::THRESH_BINARY);ではdiffの画像を2値化、つまり白黒画像にしています。
THRESH_BINARY閾値(しきいち)を超えたらmaxValueにそれ以外は0にします。
今回閾値はth、maxValueは255になっているので、特に指定がない場合は20を超えるものは255,それ以外は0となり、白黒画像できます。
出力される画像はこれです。
f:id:kisaragi211:20191102203046j:plain:w300:h300
mask画像が粗くなってしまうのは仕方がありません。
なので閾値を変更する、もしくは別の方法を使わないと綺麗な縁取りはできないと思います。多分。
(ここは私も悩んでいるところで、良い方法があれば教えてください。)

image_composition解説

ではこれで、maskを返却する関数の説明が終わったので、次はメインの処理に移りたいと思います。

  cv::Mat base_frame;
  base_frame = cv::imread(base_path.c_str());
  cv::cvtColor(base_frame, base_frame, cv::COLOR_BGR2GRAY);

  cv::Mat bg = cv::imread(bg_path.c_str());
  cv::Mat fg = cv::imread(fg_path.c_str());

ここでは単純に処理に必要な画像を読み込んでいます。
それぞれimreadで画像を読み込んでいますが、引数がchar型なのでc_str()でstringからchar型へ変換しています。
base_frameだけcvtColorという処理を行っていますが、後の指定がCOLOR_BGR2GRAYなので、色付きのものをモノクロに変更している処理になります。
元々真っ黒だから意味がないように思えますが、チャンネルを揃えるために必要な処理になります。
RGBではないことに違和感を覚えますが、OpencvはBGRの順らしいです。
これはBGR2RGBでRGBに変更もできます。

  if (x_pos + fg.cols > bg.cols) {
    x_pos = bg.cols - fg.cols;
  }
  if (y_pos + fg.rows > bg.rows) {
    y_pos = bg.rows - fg.rows;
  }
  if (x_pos < 0 || y_pos < 0)
    return -1;

ここの処理は引数で受け取った座標の修正を行っています。
今回合成にはROIという領域を前景のサイズ分確保して、そこに画像を合成するという処理を行います。
しかし、ROIの領域を確保する際に背景のサイズを超えてしまうとエラーになってしまうので座標を修正しています。
3つ目の条件分岐では前景画像の縦横幅どちらかが背景画像よりも大きくてもエラーになるので、その判定をしています。

  cv::Rect rect_roi(x_pos, y_pos, fg.cols, fg.rows);
  cv::Mat roi = bg(rect_roi);

ROI, 画像を合成する領域の確保の処理がこれになります。
Rectクラスを用いて、左上座標と縦横幅を指定します。今回は前景画像のサイズに依存します。
指定した領域を背景から切り出してroiを確保します。
この際roiは値渡しではなく参照渡しになります。(割とミソ)
確保できた領域はこうなります。
f:id:kisaragi211:20191102210510j:plain
今回座標は(0, 0)にしたので背景の左上から前景のサイズ分確保されています。

  cv::Mat fg_gray;
  cv::cvtColor(fg, fg_gray, cv::COLOR_BGR2GRAY);

  cv::Mat mask;    
  mask = frame_sub(base_frame, fg_gray, th);

ここでは前景画像をグレースケールに変換して、frame_subを行っています。
変換を行っているのはbase画像とチャンネルを揃えるためです。

チャンネルとは

  • CV_8U 画像の場合,0 から 255
  • CV_16U 画像の場合,0 から 65535
  • CV_32F 画像の場合,0 から 1

というように決まっていて、これがずれると処理できないのでチャンネルを揃える必要があります。(結構めんどくさい)


さて本題からずれましたが元に戻ります。
この処理で確保できたmaskはframe_subで出力される画像です。

  cv::Mat mask_inv;
  cv::bitwise_not(mask, mask_inv);  

ここではmask画像をbitwise_notなので白黒を反転させています。
画像を見た方が早いと思います。

f:id:kisaragi211:20191102210915j:plainf:id:kisaragi211:20191102203046j:plain

左側が反転した後の画像、mask_invです。右は通常のmaskになります。

  cv::Mat fg_down;
  cv::bitwise_and(roi, roi, fg_down, mask_inv); 

mask_invを使ってマスク処理を施してfg_downを作成します。
fg_downは前景の背景になります。言ってもわかりづらいので画像を載せます。
f:id:kisaragi211:20191102211120j:plain
まあ、見たら何を言ってるのかわかったと思います。
勘の良い方はお気づきだと思いますが、ここまで来れば合成できたも同然です。
一応先にコードを載せます。

  cv::add(fg, fg_down, roi);

前景画像にロゴの形がくり抜かれた画像を重ねているだけです。
ざっくりした説明ですが、イメージ付きやすいと思います。
一応ピクセル毎の合計を求める処理らしいです。

cv::imwrite(result_path.c_str(), bg);

最後にresult_pathで画像を書き出して終了です。
bgに処理を加えていないから出力は変わらないんじゃないか、と思いますが。
addした時の出力先がroiになっていて、roiはbgの領域を確保したものなのでroiを変更するとbgも変更されるという流れになっています。
なので、roiという別領域を画像情報として保持したのではなく、bg画像内での作業スペースを確保したというイメージの方がわかりやすいかと思います。
結果はこうなります。
f:id:kisaragi211:20191102212742j:plain

まとめ

全体的な流れとしては
背景からroi(作業場所)を指定する->roiに前景画像を貼り付ける->roiに前景の背景を加える->書き出して終了
という流れです。
間違った解釈が多い気がしますので、指摘していただけるとありがたいです。
pathの指定等が前回記事のimage_to_video等と違うのでpathがよーわからん、となったら以下を参照してください。

github.com

ここにmainを含めたコードを置いておくので参考にしてもらえればと思います。

p.s.
画像処理まだまだよくわからん!