膚色檢測演算法
由於能力有限,演算法層面的東西自己去創新的很少,很多都是從現有的論文中學習,然後實踐的。
首先列一些現在主流的面板檢測的方法都有哪些:
- 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
每個演算法的思想都是大同小異的,都是根據總結出來的一些經驗,設定面板顏色的範圍,再將其過濾出來,不同的只是過濾的過程在不同的顏色空間下進行而已。我們可以根據自己的應用場景,適當地修改這些範圍,以獲得滿意的結果。可以改善的方向就是,我們可以用合適的濾波器或者形態學處理一些噪聲,來使得提取出來的面板更為乾淨。
1.基於RGB顏色空間的簡單閾值膚色識別
在human skin color clustering for face detection一文中提出如下簡單的判別算式:
R>95 And G>40 And B>20 And R>G And R>B And Max(R,G,B)-Min(R,G,B)>15 And Abs(R-G)>15
膚色在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範圍來判定面板的演算法太受光線的影響了,魯棒性確實不好。
2.基於橢圓面板模型的面板檢測
經過前人學者大量的面板統計資訊可以知道,如果將面板資訊對映到YCrCb空間,則在CrCb二維空間中這些面板畫素點近似成一個橢圓分佈。因此如果我們得到了一個CrCb的橢圓,下次來一個座標(Cr, Cb)我們只需判斷它是否在橢圓內(包括邊界),如果是,則可以判斷其為面板,否則就是非面板畫素點。
這種基於膚色橢圓模型的演算法的面板檢測較上面演算法在效果上有著較大的提升。
3.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法)
效果:
可以看出這種方法效果也還不錯。
4.基於YCrCb顏色空間Cr,Cb範圍篩選法
這個方法跟法一其實大同小異,只是顏色空間不同而已。據資料顯示,正常黃種人的Cr分量大約在133至173之間,Cb分量大約在77至127之間。大家可以根據自己專案需求放大或縮小這兩個分量的範圍,會有不同的效果。
5.HSV顏色空間H範圍篩選法
同樣地,也是在不同的顏色空間下采取相應的顏色範圍將面板分割出來。
6.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,則表示先進行一次腐蝕操作,後進行一次膨脹操作。
從效果圖看來,背景多了很多白色的雜質,單從直觀效果來看,貌似還不如上面寫的幾個演算法。
完整演算法程式碼如下:
#include "stdafx.h"
#include <iostream>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <cvaux.h>
#include <highgui.h>
using namespace cv;
#if 1
class skin_detector
{
public:
/*基於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;
}
/*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;
}
/*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;
}
/*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;
}
/*基於橢圓面板模型的面板檢測*/
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;
}
/*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;
}
};
#endif
測試程式碼:
int main(int argc, char** argv)
{
VideoCapture cap(0);
if (!cap.isOpened())
{
printf("fail to open camera!\n");
return -1;
}
Mat frame;
skin_detector detector;
while (1)
{
cap >> frame;
if (frame.empty())
{
continue;
}
Mat skin = detector.AdaptiveSkinDetector_detect(frame);//AdaptiveSkinDetector_detect
imshow("AdaptiveSkinDetector capture skin", skin);
imshow("capture src",frame);
if (waitKey(1) == 27)
{
break;
}
}
return 0;
}
文章參考: