好久沒寫部落格了,因為最近都忙著趕專案和打比賽==| 好吧,今天我打算寫一篇關於使用opencv做皮膚檢測的技術總結。那首先列一些現在主流的皮膚檢測的方法都有哪些:
- RGB color space
- Ycrcb之cr分量+otsu閾值化
- YCrCb中133<=Cr<=173 77<=Cb<=127
- HSV中 7<H<20 28<S<256 50<V<256
- 基於橢圓皮膚模型的皮膚檢測
- opencv自帶膚色檢測類AdaptiveSkinDetector
那我們今天就來一一實現它吧!
方法一:基於RGB的皮膚檢測
根據RGB顏色模型找出定義好的膚色範圍內的畫素點,範圍外的畫素點設為黑色。
查閱資料後可以知道,前人做了大量研究,膚色在RGB模型下的範圍基本滿足以下約束:
在均勻光照下應滿足以下判別式:
R>95 AND G>40 B>20 AND MAX(R,G,B)-MIN(R,G,B)>15 AND ABS(R-G)>15 AND R>G AND R>B
在側光拍攝環境下:
R>220 AND G>210 AND B>170 AND ABS(R-G)<=15 AND R>B AND G>B
既然判別式已經確定了,所以按照判別式寫程式就很簡單了。
/*基於RGB範圍的皮膚檢測*/
Mat RGB_detect(Mat& img)
{
/*
R>95 AND G>40 B>20 AND MAX(R,G,B)-MIN(R,G,B)>15 AND ABS(R-G)>15 AND R>G AND R>B
OR
R>220 AND G>210 AND B>170 AND ABS(R-G)<=15 AND R>B AND G>B
*/
Mat detect = img.clone();
detect.setTo(0);
if (img.empty() || img.channels() != 3)
{
return detect;
}
for (int i = 0; i < img.rows; i++)
{
for (int j = 0; j < img.cols; j++)
{
uchar *p_detect = detect.ptr<uchar>(i, j);
uchar *p_img = img.ptr<uchar>(i, j);
if ((p_img[2] > 95 && p_img[1]>40 && p_img[0] > 20 &&
(MAX(p_img[0], MAX(p_img[1], p_img[2])) - MIN(p_img[0], MIN(p_img[1], p_img[2])) > 15) &&
abs(p_img[2] - p_img[1]) > 15 && p_img[2] > p_img[1] && p_img[1] > p_img[0]) ||
(p_img[2] > 200 && p_img[1] > 210 && p_img[0] > 170 && abs(p_img[2] - p_img[1]) <= 15 &&
p_img[2] > p_img[0] && p_img[1] > p_img[0]))
{
p_detect[0] = p_img[0];
p_detect[1] = p_img[1];
p_detect[2] = p_img[2];
}
}
}
return detect;
}
檢測效果如下:
從檢測結果可以看出,皮膚的檢測效果並不好,首先皮膚檢測的完整性並不高,一些稍微光線不好的區域也沒法檢測出皮膚來。第二,這種基於RBG範圍來判定皮膚的演算法太受光線的影響了,魯棒性確實不好。
方法二:基於橢圓皮膚模型的皮膚檢測
經過前人學者大量的皮膚統計資訊可以知道,如果將皮膚資訊對映到YCrCb空間,則在CrCb二維空間中這些皮膚畫素點近似成一個橢圓分佈。因此如果我們得到了一個CrCb的橢圓,下次來一個座標(Cr, Cb)我們只需判斷它是否在橢圓內(包括邊界),如果是,則可以判斷其為皮膚,否則就是非皮膚畫素點。
/*基於橢圓皮膚模型的皮膚檢測*/
Mat ellipse_detect(Mat& src)
{
Mat img = src.clone();
Mat skinCrCbHist = Mat::zeros(Size(256, 256), CV_8UC1);
//利用opencv自帶的橢圓生成函式先生成一個膚色橢圓模型
ellipse(skinCrCbHist, Point(113, 155.6), Size(23.4, 15.2), 43.0, 0.0, 360.0, Scalar(255, 255, 255), -1);
Mat ycrcb_image;
Mat output_mask = Mat::zeros(img.size(), CV_8UC1);
cvtColor(img, ycrcb_image, CV_BGR2YCrCb); //首先轉換成到YCrCb空間
for (int i = 0; i < img.cols; i++) //利用橢圓皮膚模型進行皮膚檢測
for (int j = 0; j < img.rows; j++)
{
Vec3b ycrcb = ycrcb_image.at<Vec3b>(j, i);
if (skinCrCbHist.at<uchar>(ycrcb[1], ycrcb[2]) > 0) //如果該落在皮膚模型橢圓區域內,該點就是皮膚畫素點
output_mask.at<uchar>(j, i) = 255;
}
Mat detect;
img.copyTo(detect,output_mask); //返回膚色圖
return detect;
}
檢測效果:
這種基於膚色橢圓模型的演算法的皮膚檢測較上面演算法在效果上有著較大的提升,基本上改檢測的皮膚都檢測到了,對光線的抗干擾能力也是比較強的,檢測出來的影像都比較乾淨,背景雜質較少。
法三:YCrCb顏色空間Cr分量+Otsu法閾值分割
這裡先簡單介紹YCrCb顏色空間。
YCrCb即YUV,其中“Y”表示明亮度(Luminance或Luma),也就是灰階值;而“U”和“V” 表示的則是色度(Chrominance或Chroma),作用是描述影像色彩及飽和度,用於指定畫素的顏色。“亮度”是透過RGB輸入訊號來建立的,方法是將RGB訊號的特定部分疊加到一起。“色度”則定義了顏色的兩個方面─色調與飽和度,分別用Cr和Cb來表示。其中,Cr反映了RGB輸入訊號紅色部分與RGB訊號亮度值之間的差異。而Cb反映的是RGB輸入訊號藍色部分與RGB訊號亮度值之間的差異。
該方法的原理也很簡單:
a.將RGB影像轉換到YCrCb顏色空間,提取Cr分量影像
b.對Cr做自二值化閾值分割處理(Otsu法)
/*YCrCb顏色空間Cr分量+Otsu法*/
Mat YCrCb_Otsu_detect(Mat& src)
{
Mat ycrcb_image;
cvtColor(src, ycrcb_image, CV_BGR2YCrCb); //首先轉換成到YCrCb空間
Mat detect;
vector<Mat> channels;
split(ycrcb_image, channels);
Mat output_mask = channels[1];
threshold(output_mask, output_mask, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU);
src.copyTo(detect, output_mask);
return detect;
}
檢測效果:
法四:基於YCrCb顏色空間Cr,Cb範圍篩選法
這個方法跟法一其實大同小異,只是顏色空間不同而已。據資料顯示,正常黃種人的Cr分量大約在133至173之間,Cb分量大約在77至127之間。大家可以根據自己專案需求放大或縮小這兩個分量的範圍,會有不同的效果。
/*YCrCb顏色空間Cr,Cb範圍篩選法*/
Mat YCrCb_detect(Mat & src)
{
Mat ycrcb_image;
int Cr = 1;
int Cb = 2;
cvtColor(src, ycrcb_image, CV_BGR2YCrCb); //首先轉換成到YCrCb空間
Mat output_mask = Mat::zeros(src.size(), CV_8UC1);
for (int i = 0; i < src.rows; i++)
{
for (int j = 0; j < src.cols; j++)
{
uchar *p_mask = output_mask.ptr<uchar>(i, j);
uchar *p_src = ycrcb_image.ptr<uchar>(i, j);
if (p_src[Cr] >= 133 && p_src[Cr] <= 173 && p_src[Cb] >= 77 && p_src[Cb] <= 127)
{
p_mask[0] = 255;
}
}
}
Mat detect;
src.copyTo(detect, output_mask);;
return detect;
}
檢測效果:
法五:HSV顏色空間H範圍篩選法
同樣地,也是在不同的顏色空間下采取相應的顏色範圍將皮膚分割出來。
/*HSV顏色空間H範圍篩選法*/
Mat HSV_detector(Mat& src)
{
Mat hsv_image;
int h = 0;
int s = 1;
int v = 2;
cvtColor(src, hsv_image, CV_BGR2HSV); //首先轉換成到YCrCb空間
Mat output_mask = Mat::zeros(src.size(), CV_8UC1);
for (int i = 0; i < src.rows; i++)
{
for (int j = 0; j < src.cols; j++)
{
uchar *p_mask = output_mask.ptr<uchar>(i, j);
uchar *p_src = hsv_image.ptr<uchar>(i, j);
if (p_src[h] >= 0 && p_src[h] <= 20 && p_src[s] >=48 && p_src[v] >=50)
{
p_mask[0] = 255;
}
}
}
Mat detect;
src.copyTo(detect, output_mask);;
return detect;
}
檢測效果:
法六:opencv自帶膚色檢測類AdaptiveSkinDetector
opencv提供了下面這個好用的皮膚檢測函式:
CvAdaptiveSkinDetector(int samplingDivider = 1, int morphingMethod = MORPHING_METHOD_NONE);
這個函式的第二個參數列示皮膚檢測過程時所採用的圖形學操作方式,其取值有3種可能:
- 如果為MORPHING_METHOD_ERODE,則表示只進行一次腐蝕操作;
- 如果為MORPHING_METHOD_ERODE_ERODE,則表示連續進行2次腐蝕操作;
- 如果為MORPHING_METHOD_ERODE_DILATE,則表示先進行一次腐蝕操作,後進行一次膨脹操作。
/*opencv自帶膚色檢測類AdaptiveSkinDetector*/
Mat AdaptiveSkinDetector_detect(Mat& src)
{
IplImage *frame;
frame = &IplImage(src); //Mat -> IplImage
CvAdaptiveSkinDetector filter(1, CvAdaptiveSkinDetector::MORPHING_METHOD_ERODE_DILATE);
IplImage *maskImg = cvCreateImage(cvSize(src.cols, src.rows), IPL_DEPTH_8U, 1);
IplImage *skinImg = cvCreateImage(cvSize(src.cols, src.rows), IPL_DEPTH_8U, 3);
cvZero(skinImg);
filter.process(frame, maskImg); // process the frame
cvCopy(frame, skinImg, maskImg);
Mat tmp(skinImg); //IplImage -> Mat
Mat detect = tmp.clone();
cvReleaseImage(&skinImg);
cvReleaseImage(&maskImg);
return detect;
}
從效果圖看來,背景多了很多白色的雜質,單從直觀效果來看,貌似還不如我上面寫的幾個演算法,難道跟場景有關係?當然,opencv自帶的這個api在皮膚檢測上確實相當不錯的。
總結
今天花了將近7個小時才擼了這篇文章出來==!這篇文章對各大主流的皮膚檢測演算法做了個總結和實現。其實說白了,每個演算法的思想都是大同小異的,都是根據總結出來的一些經驗,設定皮膚顏色的範圍,再將其過濾出來,不同的只是過濾的過程在不同的顏色空間下進行而已。我們可以根據自己的應用場景,適當地修改這些範圍,以獲得滿意的結果。可以改善的方向就是,我們可以用合適的濾波器或者形態學處理一些噪聲,來使得提取出來的皮膚更為乾淨。
完整程式碼可以訪問我的github來獲取~