Keying,即抠像,从一幅图像中提取所要的前景,让它与背景分离起来。Key通常只包含一个通道,是一幅黑白图像,通常黑色的区域(0)代表完全透明,而白色区域(1)代表完全不透明,而灰色区域表示着半透明。
本实验用到以下抠像技术:
亮度抠像一般用于画面上有明显亮度差异的镜头抠像,是基于lab中的L通道的抠像技术,对于明暗反差很大的图像我们应用这种抠像技术使背景透明。如明亮天空背景下拍摄的画面,就可以利用抠像将天空去除替换成想要的动态天空素材进行再编辑等等。
以上图为例,在Lab (L亮度, ab色度)通道下的各通道图如下:
可以看到L通道背景和人物的差异值很明显,可以用于区分前景和背景。
色度抠像又称色度键,是基于RGB模式的抠像技术,其从原理上最接近最初的蓝绿幕技术,即通过前景和背景颜色差异将背景从画面中抠除并替换。
当然这种抠像方法有一个小问题就是前景如果出现背景色也会被抠除,有可能会出现内部零散的点。这可以通过形态学的膨胀、腐蚀操作进行改善。
差值抠像比较特殊,其原理是通过寻找两段同机位拍摄的画面的差别并将其保留,而将没有差别画面作为背景去除。其基本思想是,先把前景物体和背景一起拍摄下来,然后保持机位不变,去掉前景物体,单独拍摄背景。这样拍摄下来的两个画面相比较,在理想状态下,背景部分是完全相同的,而前景出现的部分则是不同的,这些不同的部分就是需要保留的通道。
事实上这就是之前做过的实验背景相减:
通过如上两张图片得到如下掩膜:
一般这种抠像方式主要用于无法运用蓝/绿幕抠像的场景。
本实验中使用的是Color Difference Keying,名字相似但思想和上面完全不一样,计算的是RGB中三个通道的差异,根据差异程度来判断前景和背景,可应用于绿幕抠图。
三维抠像原理是将图像的色彩区域规整为三维空间显示模式,将RGB三个通道视为三维空间的三个坐标系,颜色的差异程度用空间距离来衡量。
选择哪一种抠像手段要取决于素材,而且素材对前期的拍摄有一些要求,所以在抠像之前先要对素材进行分析。分析它的亮度范围,分析它的色彩,分析它的遮挡关系,分析要提取的范围以及背景等。通常一种抠像手段是很难达到要求的,这时候就需要多种手段并用,达到最终目的。
采用蓝(绿)幕抠像的方法,提取测试图像或视频中的前景,并与新背景合成。
测试图像或视频可以从bm.avi中截取,下面是部分视频片段(CSDN放不了本地视频资源,放下载还收费!):
整个实验的流程如下:
①打开视频文件
②提取视频文件的每一帧,进行抠图操作,得到alpha通道的掩膜mask
③通过掩膜将原视频的帧与背景进行融合
④显示前景和背景融合后的帧
⑤读取下一帧,重复②
代码框架如下:
//背景图
background = imread("room_cut.jpg");
//打开视频文件
VideoCapture capture;
capture.open("source3.mp4");
if (!capture.isOpened())
{
cout << "无法打开视频文件!" << endl;
return -1;
}
//命名窗口
namedWindow("输入视频", WINDOW_AUTOSIZE);
namedWindow("输出视频", WINDOW_AUTOSIZE);
while (capture.read(frame))
{
//对每一帧提取的图像进行抠图操作,得到alpha通道的mask
Keying();
//将背景融合
Mat result = replace_and_blend(mask);
//中途退出
char c = waitKey(1);
if (c == 27)
{
break;
}
//imshow("mask", mask);
imshow("输出视频", result);
imshow("输入视频", frame);
}
waitKey(0);
destroyAllWindows();
return 0;
Luma Keying一般针对亮度L通道进行抠图,用于明暗差异大的图片,但也不仅仅适用于亮度差异大的图片,它的思想可以推广至某一通道有较大差异的情况。
输出我们绿幕的图片的G通道如下:
可以看到,由于我们的视频素材是带有绿幕的,图像背景是绿幕所以G值较大,前景G值较小,这导致了前景和背景在G通道的数值差异较大,因此我们使用Luma Keying的思想对图片的G通道进行操作。
计算掩膜mask的公式如下:
上述公式中,表示像素在G通道的值,、表示低阈值和高阈值。 同时公式中的取值在,后续写出的所有公式都默认取值在,但在OpenCV实现时范围是。
对于上述公式的直观理解是,如果G通道的值在一个高范围之间,说明是背景,设置为0;其他情况都设置为1。
产生的结果如下:
可以看到上述公式有一个问题就是,取值只有0和1,这样导致边缘过渡不平滑。对其进行边缘软化,公式修改为如下:
其中
上述公式中多加了一个阈值参数和。对上述公式的直观理解是,当通道的值仍于高范围之间,说明是背景,设置为0;当通道的值不在高范围之内时,计算,是通道的值离两个阈值的最短距离,如果超过了阈值,说明离背景的范围很远,可以直接设为1;其他情况,则平滑过渡。 对于上述情况,平滑过渡后得到的结果如下:
可以看到不是输出的取值不只有0和1,还有平滑过渡的其他数值。
代码实现如下:
//使用LumaKeying对G通道抠图
void LumaKeying(Mat& src, Mat& mask, int threshold0, int threshold1, int C)
{
//使用G通道
split(src, channels);
//imshow("G通道图像", channels[1]);
//初始化mask
mask = Mat::zeros(src.size(), CV_8UC1);
for (int y = 0; y < channels[1].rows; y++)
{
for (int x = 0; x < channels[1].cols; x++)
{
double L = channels[1].at(y, x);
//cout << L << " ";
double d = min(abs(L - threshold0), abs(L - threshold1));
if (L > threshold0 && L < threshold1)//在阈值之间,说明是背景
mask.at(y, x) = 0;
else if (d > C)//大于C说明一定是前景
mask.at(y, x) = 255;
else//软化边缘
mask.at(y, x) = int(d * 255.0 / C);
}
//cout << endl;
}
//imshow("LumaKeying'mask", mask);
}
输出的值结果如下(T0=120,T1=200,C=10):
我们还可以直接对色调进行操作,在HSV通道中,每一种颜色是由固定的范围HSV三通道值构成的,我们只需要根据是否属于绿色范围来区分前景和背景,从而进行抠图。各种颜色在HSV的范围如下表:
黑 |
灰 |
白 |
红 |
橙 |
黄 |
绿 |
青 |
蓝 |
紫 |
||
hmin |
0 |
0 |
0 |
0 |
156 |
11 |
26 |
35 |
78 |
100 |
125 |
hmax |
180 |
180 |
180 |
10 |
180 |
25 |
34 |
77 |
99 |
124 |
155 |
smin |
0 |
0 |
0 |
43 |
43 |
43 |
43 |
43 |
43 |
43 |
|
smax |
255 |
43 |
30 |
255 |
255 |
255 |
255 |
255 |
255 |
255 |
|
vmin |
0 |
46 |
221 |
46 |
46 |
46 |
46 |
46 |
46 |
46 |
|
vmax |
46 |
220 |
255 |
255 |
255 |
255 |
255 |
255 |
255 |
255 |
只需将绿色和其他颜色区分出来即可,以下是代码:
//将每一帧从rgb转化为hsv三通道
cvtColor(frame, hsv, COLOR_BGR2HSV);
//筛选绿幕范围,筛选完后人物为黑色0,背景为白色255
inRange(hsv, Scalar(35, 43, 46), Scalar(77, 255, 255), mask);
bitwise_not(mask, mask);//取反操作,将0变为255,255变为0
输出结果如下:
同样是对颜色进行处理,上面是对HSV中的色调处理,而Color Difference Keying处理的是RGB通道。
我们从Luma keying可以知道,绿幕背景的G通道值往往较大,除了对G通道的值大小进行衡量,还可以衡量G通道与其他B、R两通道差异程度,公式如下:
其中
此时是指G通道与R、B通道中最大者的差距,可能为负数。和不再表示低高阈值。
上述公示的直观理解是:背景区域的G值往往较大,而R、B两个通道的值相对较小,因此值较大者代表越接近背景区域,当大于阈值时认为是背景;而当小于阈值,且与阈值距离大于时,说明已经离背景区域差的很远,可以认为是前景;在这之间的都进行平滑过渡。
代码实现如下:
//使用Color Difference Keying抠图
void ColorDifferenceKeying(Mat& src, Mat& mask, int threshold0, int threshold1)
{
split(src, channels);
//初始化mask
mask = Mat::zeros(src.size(), CV_8UC1);
for (int y = 0; y < channels[1].rows; y++)
{
for (int x = 0; x < channels[1].cols; x++)
{
double B = channels[0].at(y, x);
double G = channels[1].at(y, x);
double R = channels[2].at(y, x);
double d = G - max(R, B);
//cout << d << " ";
if (d > threshold0)//大于阈值T0表示背景
mask.at(y, x) = 0;
else if (threshold0 - d > threshold1)//说明是前景
mask.at(y, x) = 255;
else//软化边缘
mask.at(y, x) = int((threshold0 - d) * 255.0 / threshold1);
}
//cout << endl;
}
//imshow("Color Difference Keying", mask);
}
输出mask结果(T0=70,T1=40):
将图像的BGR三个颜色通道映射到三维坐标空间,这样对于任意一个像素都可以映射为三维空间中的一个坐标点。
那么衡量背景与前景差异程度就转化为三维坐标系中点与点的空间距离。背景因为都是绿幕或者蓝幕,所以点会聚集在某一空间区域,在更优化时可以使用聚类,本实验仅人为地给出距离阈值。
具体方法上,我们可以任意选取背景中的4个点(或更多),计算所有像素与这四个节点的欧氏距离,并取最小的一个当作是该像素与背景的距离。当距离小于某一低阈值时,说明该点与背景距离较近,认为是背景;当距离大于某一高阈值时,说明离背景很远,认为是前景;其余情况进行边缘软化。
公式如下:
其中
上述公式中,是像素的BGR颜色,是随机选取的几个背景点。
为了方便,我默认随机选取图片四个角周围的4个像素点作为背景的参考点。
代码实现如下:
//使用3D Keying抠图
void threeDKeying(Mat& src, Mat& mask, int threshold0, int threshold1)
{
//定义原图像的四个s点
Point samples[4] = { Point(100, 100),Point(src.cols - 100,src.rows - 100),Point(100,src.rows - 100),Point(src.cols - 100,100) };
//分离三个RGB通道
split(src, channels);
//初始化mask
mask = Mat::zeros(src.size(), CV_8UC1);
for (int y = 0; y < channels[1].rows; y++)
{
for (int x = 0; x < channels[1].cols; x++)
{
double B = channels[0].at(y, x);
double G = channels[1].at(y, x);
double R = channels[2].at(y, x);
double d = 1e9;
//计算与四个样本点的最短空间距离
for (int k = 0; k < 4; k++)
{
double dis = 0;
//计算距离
for (int c = 0; c < 3; c++)
{
double delta=double(src.at(samples[k].y, samples[k].x)[c])- double(channels[c].at(y, x));
dis += delta * delta;
}
d = min(d, dis);
}
//cout << d << " ";
if (d < threshold0)//与样本点的距离小于阈值T0,说明是背景
mask.at(y, x) = 0;
else if (d > threshold1)//与样本点的距离大于阈值T0,说明是前景
mask.at(y, x) = 255;
else//软化边缘
mask.at(y, x) = int((d - threshold0) * 255.0 / (threshold1 - threshold0));
}
//cout << endl;
}
//imshow("3D Keying", mask);
}
输出结果(T0=300,T1=3000):
采用以下公式对前景和背景进行融合:
其中表示合成后的图片,表示前景图,表示背景图,表示透明度,由各点抠图的得到的计算而来。 在BGR三通道的图像中,分别用上述公式处理BGR三通道,最后再合成。
代码如下:
//默认背景图和前景图尺寸大小一致
Mat replace_and_blend(Mat& mask)//融合
{
//合成后的图片
Mat result = Mat::zeros(frame.size(), frame.type());
for (int y = 0; y < frame.rows; y++)
{
for (int x = 0; x < frame.cols; x++)
{
int m = mask.at(y, x);
//权重
double weight = m / 255.0;
//混合
result.at(y, x)[0] = (frame.at(y, x)[0] * weight + background.at(y, x)[0] * (1.0 - weight));
result.at(y, x)[1] = (frame.at(y, x)[1] * weight + background.at(y, x)[1] * (1.0 - weight));
result.at(y, x)[2] = (frame.at(y, x)[2] * weight + background.at(y, x)[2] * (1.0 - weight));
}
}
return result;
}
使用如下背景图:
各种抠图方法的运行结果:
从上面可以看到luma keying的合成结果不太理想,前景边缘很多地方没有正确区分。
从上面可以看到Chroma Keying的合成结果也不太理想,前景边缘被去除掉了更多,原因很可能是前景边缘与背景相接近,光靠绿色范围识别的话很容易被判定为背景。
从上面可以看到Color Difference Keying的合成结果很不错,基本正确识别前景和背景,前景虽然仍然略有一些绿色边缘没有去除,但整体效果相比前面两种方法有了大大提升。
从上面可以看到,3D Keying的合成结果也很好。
可以看到在实现的4种方法中,Color Difference Keying和3D keying的效果最好。
Luma keying和Chroma Keying效果一般,人物前景有部分内部点没有连通,边缘也不光滑,可以对其稍微优化,进行如下形态学操作:
//对mask进行形态学操作
Mat k = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));//返回指定形状和尺寸的核用于后面的形态学操作
morphologyEx(mask, mask, MORPH_CLOSE, k); //通过闭操作 填充内部的小白点,去除干扰
erode(mask, mask, k); //腐蚀操作
GaussianBlur(mask, mask, Size(3, 3), 0, 0); //高斯模糊
结果如下:
效果提升有限,仍然不太理想。因为算法过于简单化,对于luma keying来说只考虑了一个通道误差较大,对于chroma keying来说有点“一刀切”没有考虑图片具体特征和细节。
更换背景图和视频文件:
#include
#include
using namespace std;
using namespace cv;
Mat background, frame, hsv, mask;
Mat channels[3];
//使用LumaKeying对G通道抠图
void LumaKeying(Mat& src, Mat& mask, int threshold0, int threshold1, int C)
{
//使用G通道
split(src, channels);
//imshow("G通道图像", channels[1]);
//初始化mask
mask = Mat::zeros(src.size(), CV_8UC1);
for (int y = 0; y < channels[1].rows; y++)
{
for (int x = 0; x < channels[1].cols; x++)
{
double L = channels[1].at(y, x);
//cout << L << " ";
double d = min(abs(L - threshold0), abs(L - threshold1));
if (L > threshold0 && L < threshold1)//在阈值之间,说明是背景
mask.at(y, x) = 0;
else if (d > C)//大于C说明一定是前景
mask.at(y, x) = 255;
else//软化边缘
mask.at(y, x) = int(d * 255.0 / C);
}
//cout << endl;
}
//imshow("LumaKeying'mask", mask);
}
//使用Color Difference Keying抠图
void ColorDifferenceKeying(Mat& src, Mat& mask, int threshold0, int threshold1)
{
split(src, channels);
//初始化mask
mask = Mat::zeros(src.size(), CV_8UC1);
for (int y = 0; y < channels[1].rows; y++)
{
for (int x = 0; x < channels[1].cols; x++)
{
double B = channels[0].at(y, x);
double G = channels[1].at(y, x);
double R = channels[2].at(y, x);
double d = G - max(R, B);
//cout << d << " ";
if (d > threshold0)//大于阈值T0表示背景
mask.at(y, x) = 0;
else if (threshold0 - d > threshold1)//说明是前景
mask.at(y, x) = 255;
else//软化边缘
mask.at(y, x) = int((threshold0 - d) * 255.0 / threshold1);
}
//cout << endl;
}
//imshow("Color Difference Keying", mask);
}
//使用3D Keying抠图
void threeDKeying(Mat& src, Mat& mask, int threshold0, int threshold1)
{
//定义原图像的四个s点
Point samples[4] = { Point(100, 100),Point(src.cols - 100,src.rows - 100),Point(100,src.rows - 100),Point(src.cols - 100,100) };
//分离三个RGB通道
split(src, channels);
//初始化mask
mask = Mat::zeros(src.size(), CV_8UC1);
for (int y = 0; y < channels[1].rows; y++)
{
for (int x = 0; x < channels[1].cols; x++)
{
double B = channels[0].at(y, x);
double G = channels[1].at(y, x);
double R = channels[2].at(y, x);
double d = 1e9;
//计算与四个样本点的最短空间距离
for (int k = 0; k < 4; k++)
{
double dis = 0;
//计算距离
for (int c = 0; c < 3; c++)
{
double delta=double(src.at(samples[k].y, samples[k].x)[c])- double(channels[c].at(y, x));
dis += delta * delta;
}
d = min(d, dis);
}
//cout << d << " ";
if (d < threshold0)//与样本点的距离小于阈值T0,说明是背景
mask.at(y, x) = 0;
else if (d > threshold1)//与样本点的距离大于阈值T0,说明是前景
mask.at(y, x) = 255;
else//软化边缘
mask.at(y, x) = int((d - threshold0) * 255.0 / (threshold1 - threshold0));
}
//cout << endl;
}
//imshow("3D Keying", mask);
}
Mat replace_and_blend(Mat& mask)//融合
{
//合成后的图片
Mat result = Mat::zeros(frame.size(), frame.type());
for (int y = 0; y < frame.rows; y++)
{
for (int x = 0; x < frame.cols; x++)
{
int m = mask.at(y, x);
//权重
double weight = m / 255.0;
//混合
result.at(y, x)[0] = (frame.at(y, x)[0] * weight + background.at(y, x)[0] * (1.0 - weight));
result.at(y, x)[1] = (frame.at(y, x)[1] * weight + background.at(y, x)[1] * (1.0 - weight));
result.at(y, x)[2] = (frame.at(y, x)[2] * weight + background.at(y, x)[2] * (1.0 - weight));
}
}
return result;
}
int main()
{
//背景图
background = imread("room_cut.jpg");
//打开视频文件
VideoCapture capture;
capture.open("source3.mp4");
if (!capture.isOpened())
{
cout << "无法打开视频文件!" << endl;
return -1;
}
namedWindow("输入视频", WINDOW_AUTOSIZE);
namedWindow("输出视频", WINDOW_AUTOSIZE);
while (capture.read(frame))
{
将每一帧从rgb转化为hsv三通道
//cvtColor(frame, hsv, COLOR_BGR2HSV);
筛选绿幕范围,筛选完后人物为黑色0,背景为白色255
//inRange(hsv, Scalar(35, 43, 46), Scalar(77, 255, 255), mask);
//bitwise_not(mask, mask);//取反操作,将0变为255,255变为0
//LumaKeying(frame, mask, 120, 200, 10);
ColorDifferenceKeying(frame, mask, 70, 40);
//threeDKeying(frame, mask, 300, 3000);
//对mask进行形态学操作
Mat k = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));//返回指定形状和尺寸的核用于后面的形态学操作
morphologyEx(mask, mask, MORPH_CLOSE, k); //通过闭操作 填充内部的小白点,去除干扰
erode(mask, mask, k); //腐蚀操作
GaussianBlur(mask, mask, Size(3, 3), 0, 0); //高斯模糊
//背景融合与替换
Mat result = replace_and_blend(mask);
//中途退出
char c = waitKey(1);
if (c == 27)
{
break;
}
//imshow("背景", background);
//imshow("mask", mask);
imshow("输出视频", result);
imshow("输入视频", frame);
//waitKey(0);
}
waitKey(0);
destroyAllWindows();
return 0;
}