一. 業務背景
在我們的某項業務中,需要透過自研的智慧硬體“自動化”地拍攝一組組手機的照片,這些照片有時候因為光照的因素需要考慮將背景的顏色整體替換掉,然後再呈現給 C 端使用者。這時就有背景替換的需求了。
二. 技術實現使用 OpenCV ,透過傳統的影象處理來實現這個需求。
方案一:首先想到的是使用 K-means 分離出背景色。
大致的步驟如下:
將二維影象資料線性化使用 K-means 聚類演算法分離出影象的背景色將背景與手機二值化使用形態學的腐蝕,高斯模糊演算法將影象與背景交匯處高斯模糊化替換背景色以及對交匯處進行融合處理k-平均演算法(英文:k-means clustering)源於訊號處理中的一種向量量化方法,現在則更多地作為一種聚類分析方法流行於資料探勘領域。k-平均聚類的目的是:把 n 個點(可以是樣本的一次觀察或一個例項)劃分到k個聚類中,使得每個點都屬於離他最近的均值(此即聚類中心)對應的聚類,以之作為聚類的標準。這個問題將歸結為一個把資料空間劃分為Voronoi cells的問題。
K-means 演算法思想為:給定n個數據點{x1,x2,…,xn},找到K個聚類中心{a1,a2,…,aK},使得每個資料點與它最近的聚類中心的距離平方和最小,並將這個距離平方和稱為目標函式,記為Wn,其數學表示式為:
K-means.png
K-means 演算法基本流程:
初始的 K 個聚類中心。按照距離聚類中心的遠近對所有樣本進行分類。重新計算聚類中心,判斷是否退出條件:兩次聚類中心的距離足夠小視為滿足退出條件;不退出則重新回到步驟2。int main() { Mat src = imread("test.jpg"); if (src.empty()) { printf("could not load image...\n"); return -1; } imshow("origin", src); // 將二維影象資料線性化 Mat data; for (int i = 0; i < src.rows; i++) {//畫素點線性排列 for (int j = 0; j < src.cols; j++) { Vec3b point = src.at<Vec3b>(i, j); Mat tmp = (Mat_<float>(1, 3) << point[0], point[1], point[2]); data.push_back(tmp); } } // 使用K-means聚類 int numCluster = 4; Mat labels; TermCriteria criteria = TermCriteria(TermCriteria::EPS + TermCriteria::COUNT, 10, 0.1); kmeans(data, numCluster, labels, criteria, 4, KMEANS_PP_CENTERS); // 背景與手機二值化 Mat mask = Mat::zeros(src.size(), CV_8UC1); int index = src.rows * 2 + 2; //獲取點(2,2)作為背景色 int cindex = labels.at<int>(index); /* 提取背景特徵 */ for (int row = 0; row < src.rows; row++) { for (int col = 0; col < src.cols; col++) { index = row * src.cols + col; int label = labels.at<int>(index); if (label == cindex) { // 背景 mask.at<uchar>(row, col) = 0; } else { mask.at<uchar>(row, col) = 255; } } } imshow("mask", mask); // 腐蝕 + 高斯模糊:影象與背景交匯處高斯模糊化 Mat k = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1)); erode(mask, mask, k); GaussianBlur(mask, mask, Size(3, 3), 0, 0); // 更換背景色以及交匯處融合處理 RNG rng(12345); Vec3b color; //設定的背景色 color[0] = 255;//rng.uniform(0, 255); color[1] = 255;// rng.uniform(0, 255); color[2] = 255;// rng.uniform(0, 255); Mat result(src.size(), src.type()); double w = 0.0; //融合權重 int b = 0, g = 0, r = 0; int b1 = 0, g1 = 0, r1 = 0; int b2 = 0, g2 = 0, r2 = 0; 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) = color; // 背景 } else {/* 融合處理部分 */ w = m / 255.0; b1 = src.at<Vec3b>(row, col)[0]; g1 = src.at<Vec3b>(row, col)[1]; r1 = src.at<Vec3b>(row, col)[2]; b2 = color[0]; g2 = color[1]; r2 = color[2]; b = b1 * w + b2 * (1.0 - w); g = g1 * w + g2 * (1.0 - w); r = r1 * w + r2 * (1.0 - w); result.at<Vec3b>(row, col)[0] = b; result.at<Vec3b>(row, col)[1] = g; result.at<Vec3b>(row, col)[2] = r; } } } imshow("final", result); waitKey(0); return 0;}
背景替換的效果.png
方案二:方案一的演算法並不是對所有手機都有效,對於一些淺色的、跟背景顏色相近的手機,該演算法會比較無能為力。
相近顏色替換背景的效果.png
於是換一個思路:
使用 USM 銳化演算法對影象增強再用純白色的圖片作為背景圖,和銳化之後的圖片進行影象融合。影象銳化是使影象邊緣更加清晰的一種影象處理方法。
USM(Unsharpen Mask) 銳化的演算法就是對原影象先做一個高斯模糊,然後用原來的影象減去一個係數乘以高斯模糊之後的影象,然後再把值 Scale 到0~255的 RGB 素值範圍之內。基於 USM 銳化的演算法可以去除一些細小的干擾細節和噪聲,比一般直接使用卷積銳化運算元得到的影象銳化結果更加真實可信。
int main() { Mat src = imread("./test.jpg"); if (src.empty()) { printf("could not load image...\n"); return -1; } namedWindow("src", WINDOW_AUTOSIZE); imshow("origin", src); Mat blur_img, usm; GaussianBlur(src, blur_img, Size(0, 0), 25); addWeighted(src, 1.5, blur_img, -0.5, 0, usm); imshow("usm", usm); Mat roi = Mat(Size(src.cols,src.rows), CV_8UC3, Scalar(255, 255, 255)); Mat dst; addWeighted(usm, 1.275, roi, 0.00015, 0, dst); imshow("final", dst); waitKey(0); return 0;}
其中,addWeighted 函式是將兩張大小相同、型別相同的圖片進行融合。數學公式如下:
dst = src1*alpha + src2*beta + gamma;
融合後的效果.png
三. 總結其實,我嘗試過用 OpenCV 多種方式實現該功能,也嘗試過使用深度學習實現。目前還沒有最滿意的效果。後續,我會更偏向於使用深度學習來實現該功能。
本文轉自OpenCV學堂,獨家授權