Background
通常のマスク画像を使った画像合成では、表示させたい部分を白くして行列のかけ算で処理します。(Python, OpenCV, NumPyで画像のアルファブレンドとマスク処理を参照)
しかし、alphamatで出力された画像の画素は0~255
の範囲で、0(黒)or255(白)
とくっきりと分かれてないです。ここでは、alphamatを使って背景画像と合成をかけてみようと思います。
(alphamatの画像処理方法はOpenCVでalphamatを使ってみたを参照)
Method
ポイントは3つ
- 処理する画像は
0~255
から0.0~1.0
に標準(ノルム)化にする - アルファ値(透過度)のまま画像とかけ算をする
- マスクした箇所と背景の箇所をそれぞれ別個で処理する
処理する画像は0~255
から0.0~1.0
に標準(ノルム)化にする
次のアルファ値とのかけ算し易いように0.0~1.0
に変換します。おそらく標準では CV_8UC3
(unsigned char 8bit - 3チャネル)
なのでfloat型 CV_32FC3
にして、 1/255
にします。
例)
srcMat.convertTo(srcMat, CV_32FC3, 1.0/255);
また、 もし処理結果を保存したいときは0.0~1.0
のままだと真っ黒な画像が出来てしまうので、CV_8UC3
に再変換する必要があります。
例)
srcMat.convertTo(srcMat, CV_8UC3, 255);
アルファ値(透過度)のまま画像とかけ算をする
各画素同士のかけ算をする場合は cv::multiply
を使います。 出力結果は3つ目の引数に反映されます。
cv::multiply(maskMat, srcMat, srcMat);
マスクした箇所と背景の箇所をそれぞれ別個で処理する
今回の場合はマスク画像の白黒によって、表示するorしないといった判定だけではなくアルファ値によって表示を調整しています。例としてあるマスク画素のアルファ値が20%の場合は、表示画素20%:背景画素80%の配分で合成します。
cv::multiply(maskMat, srcMat, srcMat);
cv::multiply(cv::Scalar::all(1.0)-maskMat, backMat, backMat);
cv::add(srcMat, backMat, dstMat);
Process
c++
# include <iostream>
# include <opencv2/core.hpp>
# include <opencv2/core/utility.hpp>
# include <opencv2/imgproc.hpp>
# include <opencv2/videoio.hpp>
# include <opencv2/highgui.hpp>
# include <opencv2/alphamat.hpp>
int main(int argc, const char* argv[])
{
cv::String keys = "{src||}""{back||}""{mask||}";
cv::CommandLineParser parser(argc, argv, keys);
cv::String src_path = parser.get<cv::String>("src");
cv::String mask_path = parser.get<cv::String>("mask");
cv::String back_path = parser.get<cv::String>("back");
std::string name = "Effect";
cv::Mat srcMat = cv::imread(src_path, cv::IMREAD_COLOR);
cv::Mat maskMat = cv::imread(mask_path);
cv::Mat backMat = cv::imread(back_path, cv::IMREAD_COLOR);
srcMat.convertTo(srcMat, CV_32FC3, 1.0/255);
backMat.convertTo(backMat, CV_32FC3, 1.0/255);
maskMat.convertTo(maskMat, CV_32FC3, 1.0/255);
cv::Mat dstMat = cv::Mat::zeros(srcMat.size(), srcMat.type());
cv::multiply(maskMat, srcMat, srcMat);
cv::multiply(cv::Scalar::all(1.0)-maskMat, backMat, backMat);
cv::add(srcMat, backMat, dstMat);
srcMat.convertTo(srcMat, CV_8UC3, 255);
backMat.convertTo(backMat, CV_8UC3, 255);
dstMat.convertTo(dstMat, CV_8UC3, 255);
cv::namedWindow(name, cv::WINDOW_AUTOSIZE);
while(true){
cv::imshow("src-mask", srcMat);
cv::imshow("dst-mask", backMat);
cv::imshow(name, dstMat);
int key = cv::waitKey(0);
if (key == 27){
break;
} else if(key == 's'){
cv::imwrite("output_front.png", srcMat);
cv::imwrite("output_back.png", backMat);
cv::imwrite("output_blend.png", dstMat);
}
}
cv::destroyAllWindows();
return 0;
}
./blend -src=[元画像] -mask=[マスク画像] -back=[背景画像]
Consequence
読み込んだ画像
元画像 | trimap(マスク画像) | 背景画像 |
---|---|---|
![]() |
![]() |
![]() |
trimap(マスク画像)はちょっと変えています。髪全体にすると黒い部分が残ってしまい、背景画素が使われるので、輪郭部分を灰色にしています。
処理中の画像
|元画像 × マスク画像|背景画像 × マスク画像|
|:--:|:--:|:--:|
||
|
処理した画像

輪郭に白い部分が残って少し気になりますが、微細な輪郭を残したまま合成ができていることがわかります。
PostScript
今回の輪郭の設定はアノテーションツールを使って手動でセットしました。
DNNを使って物体をセグメンテーション(領域分割)してくれる方法があるので、輪郭の部分を灰色にしたtrimapを作成することで自動化ができそうです。
しかし、今回使った高解像度の画像を使うと処理時間に数分かかります。