最近在小红书上看到很多更换证件照背景色的需求,联想到以前自己也更换过证件照背景色而且还是付费的,碰巧最近在看一本书《Java+OpenCV高效入门》,于是查找资料,找到了通过技术解决这个需求的办法。
先看效果图(图片来自网络,如有侵权,请联系我删除):
下面直接上代码,只需要一个函数就可以了:
/**
* 算法实现步骤:
* 1、加载原图像
* 2、制作kmeans输入参数所需要的数据(kmeans的输入数据类型是CV_32F,所以不能直接使用原始图像的数据,因为原始图像的数据类型为CV_8UC1)
* 3、使用kmeans算法实现图像分类,并得到分类标签
* 4、创建遮罩:通过分类标签,将背景部分的颜色标记位0,将前景(人物)像素值标记位255
* 5、先对mask执行形态学操作去除干扰的白点,在使用高斯模糊平滑前景和背景之前的过度
* 6、创建一个3通道的目标输出结果Mat,然后将目标背景填充到背景区域,将前景部分填充到前景区域。
* 7、输出图像
* ps:算法的核心步骤其实就是找到mask,当mask找到之后就可以使用分类标签将背景和前景替换成为自己想要的像素。
* 参考:https://www.cnblogs.com/tony-yang-flutter/p/16153446.html
* @param photoPath 本地图片路径
* @param rgbEnd 目标背景色
* @return
*/
@Override
public Mat changePhotoBackgroundColor(String photoPath, double[] rgbBeg, double[] rgbEnd) {
Mat src = Imgcodecs.imread(photoPath);
//制作kmeans需要的数据
int width = src.cols();
int height = src.rows();
int dims = src.channels();
int sampleCount = width*height;//总共的像素点
Mat points = new Mat(sampleCount, dims, CvType.CV_32F,new Scalar(10));
int index = 0;
for(int row = 0;row<height;row++){
for(int col = 0;col<width;col++){
index = row * width + col;
double[] bgr = src.get(row, col);
points.put(index,0, bgr[0]);
points.put(index,1, bgr[1]);
points.put(index,2, bgr[2]);
}
}
int numCluster = 4;//多少个分类
Mat labels = new Mat();//分类标签
Mat centers = new Mat();//中心点
TermCriteria criteria = new TermCriteria(TermCriteria.EPS + TermCriteria.COUNT, 10, 0.1);
kmeans(points,numCluster,labels,criteria,3,KMEANS_PP_CENTERS,centers);
//创建遮罩
Mat mask = Mat.zeros(src.size(),CvType.CV_8UC1);
//找到背景像素的像素点位置
index = src.rows() * 2 + 2;
//找到像素点位置在labels中所对应的标签,找到这个标签以后就可以根据这个标签来判断前景和背景
double[] cIndex = labels.get(index,0);
for(int row=0;row<height;row++){
for(int col=0;col<width;col++){
index = row*width+col;
double[] label = labels.get(index, 0);
if(label[0] == cIndex[0]){//背景
mask.put(row, col, 0);
}else{//前景
mask.put(row, col, 255);
}
}
}
//使用形态学腐蚀操作取出遮罩中的可能干扰正常结果的白点
Mat kernel = getStructuringElement(MORPH_RECT,new Size(3,3),new Point(-1,-1));
erode(mask,mask,kernel);
//使用高斯模糊平滑边缘像素
GaussianBlur(mask,mask,new Size(3,3),0,0);
//执行图像像素融合,执行最终的背景替换,定义背景颜色
double[] bgColor = new double[3];
bgColor[0] = rgbEnd[2];
bgColor[1] = rgbEnd[1];
bgColor[2] = rgbEnd[0];
//定义一个空的彩色图片
Mat result = Mat.zeros(src.size(),CvType.CV_8UC3);
//下面是背景融合的代码
double w = 0.0;
double b = 0, g = 0, r = 0;
double b1 = 0, g1 = 0, r1 = 0;
double b2 = 0, g2 = 0, r2 = 0;
for(int row = 0;row<height;row++){
for(int col=0;col<width;col++){
double[] pix = mask.get(row,col);//获取像素值
if(pix[0] == 255){//前景
result.put(row, col, src.get(row,col));
}else if(pix[0] == 0){//背景
result.put(row, col, bgColor);
}else{//需要像素融合的部分
w = pix[0] / 255.0;//权重
b1 = src.get(row,col)[0];
g1 = src.get(row,col)[1];
r1 = src.get(row,col)[2];
b2 = bgColor[0];
g2 = bgColor[1];
r2 = bgColor[2];
b = b1*w+b2*(1.0-w);
g = g1*w+g2*(1.0-w);
r = r1*w+r2*(1.0-w);
result.put(row, col, b, g, r);
}
}
}
return result;
}