作者:翟天保Steven
版權聲明:著作權歸作者所有,商業(yè)轉載請聯(lián)系作者獲得授權,非商業(yè)轉載請注明出處
實現(xiàn)原理
? ? ? 天空變換是圖像分割的一種應用,把圖像中的天空與非天空區(qū)分割開,結合掩膜將天空更改為其他圖像。如何較優(yōu)地實現(xiàn)天空變換,與識別證件照類似,難點在于兩個:
- 天空分割。將圖像轉為HSV并對S和V通道進行直方圖均衡化,再通過設定的HSV三通道閾值選定天空的顏色范圍,進而提取天空區(qū)域(H為78-124,S為0-255,V為78-255,該參數(shù)為我大量測試后憑經(jīng)驗所設,考慮到圖像多樣性,函數(shù)我提供了參數(shù)接口以便動態(tài)調整);thresh為天空區(qū)域的掩膜圖,反相后的thresh_為非天空區(qū)域掩膜圖,接下來識別非天空區(qū)的輪廓區(qū)域,采用外部輪廓方式,這樣能提取出多個輪廓區(qū),保留最大的輪廓區(qū),不出意外這個就是前景區(qū)(出意外就自己重寫該部分代碼來判斷真實的前景區(qū));然后閉運算填充輪廓區(qū)內部微小孔洞,注意這個參數(shù)越大,輪廓越完整,但代價是一些孔洞處沒法進行圖像更換,所以自己把握參數(shù);進行均值濾波,這一步是為了邊緣平滑,為后續(xù)新天空和非天空區(qū)的融合作鋪墊;輸出Foreground,該掩膜圖255的區(qū)域為非天空區(qū)。
- 兩區(qū)域邊緣融合。如果不能很好地融合,就能看出明顯的摳圖痕跡,所以融合是很關鍵的一步。首先,將新天空圖尺寸調整為原圖尺寸;其次,對蒙版區(qū)(掩膜)進行均值濾波,其邊緣區(qū)會生成介于0-255之間的緩存區(qū);再通過比例分配的方式對緩存區(qū)的像素點上色,我固定的比例為前景0.3天空0.7,可以使得緩存區(qū)顏色傾向于天空色,且實現(xiàn)較好地過渡;最后,蒙版為0的區(qū)域為新天空圖,蒙版為255的區(qū)域不變。
? ? ? ?至此,完成了天空變換。C++實現(xiàn)代碼如下。
功能函數(shù)代碼
// 天空分離
cv::Mat SkySeparation(cv::Mat src, Inputparama input)
{
// 異常數(shù)值修正
input.low_h = max(uchar(0), min(uchar(255), input.low_h));
input.high_h = max(uchar(0), min(uchar(255), input.high_h));
input.low_s = max(uchar(0), min(uchar(255), input.low_s));
input.high_s = max(uchar(0), min(uchar(255), input.high_s));
input.low_v = max(uchar(0), min(uchar(255), input.low_v));
input.high_v = max(uchar(0), min(uchar(255), input.high_v));
input.close_size= max(0, min(10, input.close_size));
input.blur_size = max(0, min(10, input.blur_size));
// 轉為hsv通道
cv::Mat hsv,nhsv,thresh;
cvtColor(src, hsv, COLOR_BGR2HSV);
vector<cv::Mat> hsvs;
split(hsv, hsvs);
cv::Mat h,s,v;
// 直方圖均衡化
equalizeHist(hsvs[1], s);
equalizeHist(hsvs[2], v);
hsvs[1] = s.clone();
hsvs[2] = v.clone();
merge(hsvs, nhsv);
// 按天空色選出mask并反相
cv::Mat low=(cv::Mat_<uchar>{ input.low_h, input.low_s, input.low_v });
cv::Mat high = (cv::Mat_<uchar>{ input.high_h, input.high_s, input.high_v });
inRange(nhsv, low, high, thresh);
cv::Mat thresh_ = 255 - thresh;
// 尋找輪廓,找出最大輪廓作為前景圖
vector<vector<Point>> contour;// , ncontour;
vector<Vec4i> hierarchy;
findContours(thresh_, contour, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_NONE);
cv::Mat Foreground=thresh_.clone();
if (!contour.empty() && !hierarchy.empty())
{
int max = 0;
std::vector<std::vector<cv::Point> >::const_iterator itc = contour.begin();
std::vector<std::vector<cv::Point> >::const_iterator itmax;
// 遍歷所有輪廓
int i = 1;
while (itc != contour.end())
{
double area = cv::contourArea(*itc);
if (area > max)
{
itmax = itc;
max = area;
}
itc++;
}
for (auto it = contour.begin(); it != contour.end(); it++)
{
if (it!=itmax)
{
cv::Rect rect = cv::boundingRect(cv::Mat(*it));
for (int i = rect.y; i < rect.y + rect.height; i++)
{
uchar *output_data = Foreground.ptr<uchar>(i);
for (int j = rect.x; j < rect.x + rect.width; j++)
{
// 將連通區(qū)的值置0
if (output_data[j] == 255)
{
output_data[j] = 0;
}
}
}
}
}
}
// 閉運算
cv::Mat element = getStructuringElement(MORPH_ELLIPSE, Size(2*input.close_size+1, 2 * input.close_size + 1));
cv::morphologyEx(Foreground, Foreground, MORPH_CLOSE, element);
// 濾波
cv::blur(Foreground, Foreground, Size(2 * input.blur_size + 1, 2 * input.blur_size + 1));
return Foreground;
}
C++測試代碼
#include <iostream>
#include <opencv2/opencv.hpp>
#include <time.h>
using namespace std;
using namespace cv;
// 輸入?yún)?shù)
struct Inputparama {
uchar low_h = 78; // 識別天空區(qū)域hsv顏色的最底H值
uchar high_h = 124; // 識別天空區(qū)域hsv顏色的最高H值
uchar low_s = 0; // 識別天空區(qū)域hsv顏色的最底S值
uchar high_s = 255; // 識別天空區(qū)域hsv顏色的最高S值
uchar low_v = 78; // 識別天空區(qū)域hsv顏色的最底V值
uchar high_v = 255; // 識別天空區(qū)域hsv顏色的最高V值
int close_size = 4; // 非天空區(qū)域閉運算尺寸,該值越大則區(qū)域越完整,代價是一些孔洞處沒法進行圖像更換
int blur_size = 2; // 非天空區(qū)域濾波窗口尺寸,該值越大則天空與非天空區(qū)銜接處越模糊,適當?shù)臄?shù)值可以帶來較優(yōu)的融合效果
};
cv::Mat SkySeparation(cv::Mat src, Inputparama input);
cv::Mat ImageFusion(cv::Mat src1, cv::Mat src2, cv::Mat mask);
int main()
{
cv::Mat src = imread("test3.jpg");
cv::Mat sky = imread("sky5.jpg");
Inputparama input;
input.low_h = 78;
input.high_h = 124;
input.low_s = 0;
input.high_s = 255;
input.low_v = 78;
input.high_v = 255;
input.close_size = 4;
input.blur_size = 2;
clock_t s, e;
s = clock();
cv::Mat thresh = SkySeparation(src,input);
cv::Mat result = ImageFusion(src, sky, thresh);
e = clock();
double dif = (e - s) / CLOCKS_PER_SEC;
cout << "time:" << dif << endl;
imshow("original", src);
imshow("result", result);
waitKey(0);
return 0;
}
// 天空分離
cv::Mat SkySeparation(cv::Mat src, Inputparama input)
{
// 異常數(shù)值修正
input.low_h = max(uchar(0), min(uchar(255), input.low_h));
input.high_h = max(uchar(0), min(uchar(255), input.high_h));
input.low_s = max(uchar(0), min(uchar(255), input.low_s));
input.high_s = max(uchar(0), min(uchar(255), input.high_s));
input.low_v = max(uchar(0), min(uchar(255), input.low_v));
input.high_v = max(uchar(0), min(uchar(255), input.high_v));
input.close_size= max(0, min(10, input.close_size));
input.blur_size = max(0, min(10, input.blur_size));
// 轉為hsv通道
cv::Mat hsv,nhsv,thresh;
cvtColor(src, hsv, COLOR_BGR2HSV);
vector<cv::Mat> hsvs;
split(hsv, hsvs);
cv::Mat h,s,v;
// 直方圖均衡化
equalizeHist(hsvs[1], s);
equalizeHist(hsvs[2], v);
hsvs[1] = s.clone();
hsvs[2] = v.clone();
merge(hsvs, nhsv);
// 按天空色選出mask并反相
cv::Mat low=(cv::Mat_<uchar>{ input.low_h, input.low_s, input.low_v });
cv::Mat high = (cv::Mat_<uchar>{ input.high_h, input.high_s, input.high_v });
inRange(nhsv, low, high, thresh);
cv::Mat thresh_ = 255 - thresh;
// 尋找輪廓,找出最大輪廓作為前景圖
vector<vector<Point>> contour;// , ncontour;
vector<Vec4i> hierarchy;
findContours(thresh_, contour, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_NONE);
cv::Mat Foreground=thresh_.clone();
if (!contour.empty() && !hierarchy.empty())
{
int max = 0;
std::vector<std::vector<cv::Point> >::const_iterator itc = contour.begin();
std::vector<std::vector<cv::Point> >::const_iterator itmax;
// 遍歷所有輪廓
int i = 1;
while (itc != contour.end())
{
double area = cv::contourArea(*itc);
if (area > max)
{
itmax = itc;
max = area;
}
itc++;
}
for (auto it = contour.begin(); it != contour.end(); it++)
{
if (it!=itmax)
{
cv::Rect rect = cv::boundingRect(cv::Mat(*it));
for (int i = rect.y; i < rect.y + rect.height; i++)
{
uchar *output_data = Foreground.ptr<uchar>(i);
for (int j = rect.x; j < rect.x + rect.width; j++)
{
// 將連通區(qū)的值置0
if (output_data[j] == 255)
{
output_data[j] = 0;
}
}
}
}
}
}
// 閉運算
cv::Mat element = getStructuringElement(MORPH_ELLIPSE, Size(2*input.close_size+1, 2 * input.close_size + 1));
cv::morphologyEx(Foreground, Foreground, MORPH_CLOSE, element);
// 濾波
cv::blur(Foreground, Foreground, Size(2 * input.blur_size + 1, 2 * input.blur_size + 1));
return Foreground;
}
// 前景背景融合
cv::Mat ImageFusion(cv::Mat src1, cv::Mat src2, cv::Mat mask)
{
cv::Mat sky;
resize(src2, sky, Size(src1.cols, src1.rows));
cv::Mat result = src1.clone();
int row = src1.rows;
int col = src1.cols;
// 改色
for (int i = 0; i < row; ++i)
{
uchar *s1 = result.ptr<uchar>(i);
uchar *s2 = sky.ptr<uchar>(i);
uchar *m = mask.ptr<uchar>(i);
for (int j = 0; j < col; ++j)
{
// 蒙版為0的區(qū)域就是標準背景區(qū)
if (m[j] == 0)
{
s1[3 * j] = s2[3 * j];
s1[3 * j + 1] = s2[3 * j + 1];
s1[3 * j + 2] = s2[3 * j + 2];
}
// 不為0且不為255的區(qū)域是輪廓區(qū)域(邊緣區(qū)),需要虛化處理
else if (m[j] != 255)
{
// 邊緣處按比例上色
int newb = (s1[3 * j] * m[j] * 0.3 + s2[3 * j] * (255 - m[j])*0.7) / ((255 - m[j])*0.7 + m[j] * 0.3);
int newg = (s1[3 * j + 1] * m[j] * 0.3 + s2[3 * j + 1] * (255 - m[j])*0.7) / ((255 - m[j])*0.7 + m[j] * 0.3);
int newr = (s1[3 * j + 2] * m[j] * 0.3 + s2[3 * j + 2] * (255 - m[j])*0.7) / ((255 - m[j])*0.7 + m[j] * 0.3);
newb = max(0, min(255, newb));
newg = max(0, min(255, newg));
newr = max(0, min(255, newr));
s1[3 * j] = newb;
s1[3 * j + 1] = newg;
s1[3 * j + 2] = newr;
}
}
}
return result;
}
測試效果
? ? ? ?如源碼所示,函數(shù)輸入?yún)?shù)共有5項,其說明如下:
- 前6個參數(shù)分別為hsv三通道的最大最小值。
- close_size為閉運算尺寸,如果處理的圖像中有小樹林,建議尺寸調小,不然小樹林間的縫隙就是原圖,有點不協(xié)調。
- blur_size為濾波窗口尺寸,平滑天空與非天空區(qū)銜接處。
? ? ? ?總的來說,圖像如果有明顯天空背景,基本都能成功;天空白色區(qū)域過多可能識別不準,因為白色的hsv值和藍色差太多;天空下面有大片海水,也不太行,就識別出來不符合現(xiàn)實邏輯。
? ? ? ?源碼只有100多行,看懂原理最重要,比直接調用api更能學到知識。永遠記住,“代碼是死的,場景是多變的,而人是活的。”,針對不同場景,合理改寫代碼,才能產(chǎn)出最適合你的代碼。
? ? ? ?如果函數(shù)有什么可以改進完善的地方,非常歡迎大家指出,一同進步何樂而不為呢~
? ? ? ?如果文章幫助到你了,可以點個贊讓我知道,我會很快樂~加油!