证件照替换背景色是一个很常用的功能,原理其实很简单,其实是一个图像分割问题,找到图像背景区域,然后替换成新的背景,注意最后要在人物边缘做羽化处理,使其看起来过度自然,这里用多种图像分割方法实现。
- K-Means
int main(int argc, char ** argv)
{
Mat src = imread("./demo.jpg", IMREAD_COLOR);
// 图片太大,先缩放,减少运算量
cv::resize(src, src, src.size() / 2, 0, 0);
namedWindow("src image");
imshow("src image", src);
// 图像分割
const int sampleCount = src.rows * src.cols;
Mat data(sampleCount, src.channels(), CV_32F);
for (int r = 0; r < src.rows; r++)
{
for (int c = 0; c < src.cols; c++)
{
float blue = src.at<Vec3b>(r, c)[0];
float green = src.at<Vec3b>(r, c)[1];
float red = src.at<Vec3b>(r, c)[2];
int index = r * src.cols + c;
data.at<Vec3f>(index,0) = Vec3f(blue,green,red);
}
}
const int nCluster = 5; // 分为5类
Mat bestLabels(sampleCount,1,CV_32S);
// K-means图像分割
TermCriteria citeria = TermCriteria(TermCriteria::EPS + TermCriteria::COUNT, 10, 0.1);
cv::kmeans(data, nCluster, bestLabels,citeria, 3, KMEANS_PP_CENTERS);
int bgIndex = 5 * src.cols + 5; // 背景色序号
int bgLabel = bestLabels.at<int>(bgIndex, 0);
Vec3b newBgColor(217, 60, 160); // 新的背景色
Mat mask = Mat::zeros(src.size(), CV_8UC1); // 背景为0,前景为255
for (int r = 0; r < src.rows; r++)
{
for (int c = 0; c < src.cols; c++)
{
int index = r * src.cols + c;
int label = bestLabels.at<int>(index, 0);
if (label != bgLabel)
{
mask.at<uchar>(r, c) = 255;
}
}
}
// 腐蚀 + 高斯模糊,羽化,图像与背景交汇处高斯模糊化
Mat k = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
erode(mask, mask, k);
GaussianBlur(mask, mask, Size(3, 3), 0, 0);
// 更换背景色以及交汇处融合处理
Mat result(src.size(), src.type());
for (int row = 0; row < src.rows; row++) {
for (int col = 0; col < src.cols; col++) {
int m = mask.at<uchar>(row, col);
if (m == 255) {
result.at<Vec3b>(row, col) = src.at<Vec3b>(row, col); // 前景
}
else if (m == 0) {
result.at<Vec3b>(row, col) = newBgColor; // 背景
}
else {/* 融合处理部分 背景前景交界处 */
double w = m / 255.0;
int b1 = src.at<Vec3b>(row, col)[0];
int g1 = src.at<Vec3b>(row, col)[1];
int r1 = src.at<Vec3b>(row, col)[2];
int b2 = newBgColor[0];
int g2 = newBgColor[1];
int r2 = newBgColor[2];
int b = b1 * w + b2 * (1.0 - w);
int g = g1 * w + g2 * (1.0 - w);
int r = r1 * w + r2 * (1.0 - w);
result.at<Vec3b>(row, col) = Vec3b(b, g, r);
}
}
}
namedWindow("result");
imshow("result", result);
waitKey(0);
return 0;
}
- GMM
int main(int argc, char ** argv) { Mat src = imread("./demo.jpg", IMREAD_COLOR); // 图片太大,先缩放,减少运算量 cv::resize(src, src, src.size() / 2, 0, 0); namedWindow("src image"); imshow("src image", src); // 图像分割 const int sampleCount = src.rows * src.cols; Mat data(sampleCount, src.channels(), CV_32F); for (int r = 0; r < src.rows; r++) { for (int c = 0; c < src.cols; c++) { float blue = src.at<Vec3b>(r, c)[0]; float green = src.at<Vec3b>(r, c)[1]; float red = src.at<Vec3b>(r, c)[2]; int index = r * src.cols + c; data.at<Vec3f>(index,0) = Vec3f(blue,green,red); } } const int nCluster = 5; // 分为5类 Mat bestLabels(sampleCount,1,CV_32S); //GMM分割(基于高斯混合模型的期望最大值) Ptr<EM> em = EM::create(); em->setClustersNumber(nCluster); em->setCovarianceMatrixType(EM::COV_MAT_SPHERICAL); em->setTermCriteria(TermCriteria(TermCriteria::COUNT + TermCriteria::EPS, 100, 0.1)); em->trainEM(data, noArray(), bestLabels, noArray()); int bgIndex = 5 * src.cols + 5; // 背景色序号 int bgLabel = bestLabels.at<int>(bgIndex, 0); Vec3b newBgColor(217, 60, 160); // 新的背景色 Mat mask = Mat::zeros(src.size(), CV_8UC1); // 背景为0,前景为255 for (int r = 0; r < src.rows; r++) { for (int c = 0; c < src.cols; c++) { int index = r * src.cols + c; int label = bestLabels.at<int>(index, 0); if (label != bgLabel) { mask.at<uchar>(r, c) = 255; } } } // 腐蚀 + 高斯模糊,羽化,图像与背景交汇处高斯模糊化 Mat k = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1)); erode(mask, mask, k); GaussianBlur(mask, mask, Size(3, 3), 0, 0); // 更换背景色以及交汇处融合处理 Mat result(src.size(), src.type()); for (int row = 0; row < src.rows; row++) { for (int col = 0; col < src.cols; col++) { int m = mask.at<uchar>(row, col); if (m == 255) { result.at<Vec3b>(row, col) = src.at<Vec3b>(row, col); // 前景 } else if (m == 0) { result.at<Vec3b>(row, col) = newBgColor; // 背景 } else {/* 融合处理部分 背景前景交界处 */ double w = m / 255.0; int b1 = src.at<Vec3b>(row, col)[0]; int g1 = src.at<Vec3b>(row, col)[1]; int r1 = src.at<Vec3b>(row, col)[2]; int b2 = newBgColor[0]; int g2 = newBgColor[1]; int r2 = newBgColor[2]; int b = b1 * w + b2 * (1.0 - w); int g = g1 * w + g2 * (1.0 - w); int r = r1 * w + r2 * (1.0 - w); result.at<Vec3b>(row, col) = Vec3b(b, g, r); } } } namedWindow("result"); imshow("result", result); waitKey(0); return 0; }
- watershed
// watershed
Mat mask; // watershed 标记
void onMouse(int event, int x, int y, int flags, void *) {
if (event == CV_EVENT_LBUTTONDOWN) // 左键选择背景
{
mask.at<int>(y, x) = 1;
}
else if (event == CV_EVENT_RBUTTONDOWN)
{
mask.at<int>(y, x) = 2;
}
}
int main(int argc, char **argv)
{
Mat src = imread("./demo.jpg", IMREAD_COLOR);
// 图片太大,先缩放,减少运算量
cv::resize(src, src, src.size() / 2, 0, 0);
namedWindow("src image");
imshow("src image", src);
Mat gray;
cvtColor(src, gray,CV_BGR2GRAY);
// 用户参与,用户选择背景颜色
// create mask
mask = Mat::zeros(src.size(), CV_32SC1);
setMouseCallback("src image", onMouse, 0);
while (true)
{
char c = waitKey(150);
if (c == 27) // ESC
{
break;
}
else if (c == 13) // enter
{
break;
}
}
watershed(src, mask);
// 替换背景颜色
Vec3b newBgColor(217, 60, 160); // 新的背景色
Mat result;
src.copyTo(result);
for (int r = 0; r < result.rows; r++)
{
for (int c = 0; c < result.cols; c++)
{
if (mask.at<int>(r, c) == 1)
{
result.at<Vec3b>(r, c) = newBgColor;
}
}
}
namedWindow("watershed");
imshow("watershed", result);
waitKey(0);
return 0;
}
- Grubcut
void onMouse(int event, int x, int y, int flags, void* userdata);
Rect rect;
Mat src, roiImg, result;
void showImg();
bool fineTuned = false;
Mat mask;
int main(int arc, char** argv)
{
src = imread("./demo.jpg", IMREAD_COLOR);
// 图片太大,先缩放,减少运算量
cv::resize(src, src, src.size() / 2, 0, 0);
namedWindow("src image");
imshow("src image", src);
Mat result;
src.copyTo(result);
namedWindow("grubCut");
imshow("grubCut", result);
setMouseCallback("grubCut", onMouse);
//定义输出结果,结果为:GC_BGD =0(背景),GC_FGD =1(前景),GC_PR_BGD = 2(可能的背景), GC_PR_FGD = 3(可能的前景)
mask = Mat::zeros(src.size(), CV_8UC1);
while (true)
{
//两个临时矩阵变量,作为算法的中间变量使用
Mat bgModel, fgModel;
char c = waitKey(0);
Mat foreMask;
if (c == 27) // ESC
{
break;
}
else if (c == 13) // enter
{
// GrabCut 抠图
grabCut(src, mask, rect, bgModel, fgModel, 3, GC_INIT_WITH_RECT);
//比较mask的值为可能的前景像素才输出到mask中
compare(mask, GC_PR_FGD, foreMask, CMP_EQ);
fineTuned = true;
}
else if (c == 'r') // 进一步细化
{
// GrabCut 抠图
grabCut(src, mask, rect, bgModel, fgModel, 1, GC_INIT_WITH_MASK);
//比较mask的值为可能的前景像素才输出到mask中
compare(mask, GC_PR_FGD, foreMask, CMP_EQ);
}
//将原图像src中的result区域拷贝到foreground中
// 新的背景色 217,60,160
//Mat result(src.size(), src.type(), Scalar(217, 60, 160));
result = Scalar(217, 60, 160);
src.copyTo(result, foreMask);
imshow("grubCut", result);
}
waitKey(0);
return 0;
}
void showImg() {
src.copyTo(roiImg);
rectangle(roiImg, rect, Scalar(0, 0, 255), 2);
imshow("src image", roiImg);
}
//鼠标选择矩形框左上角点
void onMouse(int event, int x, int y, int flags, void* userdata)
{
if (fineTuned)
{
mask.at<uchar>(y, x) = GC_PR_BGD;
}
else
{
switch (event)
{
case CV_EVENT_LBUTTONDOWN://鼠标左键按下事件
rect.x = x;
rect.y = y;
break;
case EVENT_LBUTTONUP://鼠标弹起事件
rect = Rect(Point(0, rect.y), Point(src.cols, src.rows));
showImg();
break;
default:
break;
}
}
}