opencv的操作

目录

常见操作:

图片复制

创建一张全黑的图片

距离:

获取两条直线的距离

点到直线的距离

视频输入

思路

取某一区域的图片

思路:

代码:

显示时固定窗口大小

代码

画线和画圆和画矩形

画线

画圆

画矩形

画多边形

边缘检测

Canny检测

在图片上写文字

读取视频并保留每一帧图片取不同的名字

图像灰度化

图像二值化

方法一:固定一个阈值

方法二:计算像素的平均值

方法三:直方图确定阈值

方法四:使用近似一维Means方法寻找二值化阈值

方法五:伯恩森算法

方法六:大津算法

肤色检测

方法一:基于RGB的皮肤检测

方法二:基于椭圆皮肤模型的皮肤检测

法三:YCrCb颜色空间Cr分量+Otsu法阈值分割

法四:基于YCrCb颜色空间Cr,Cb范围筛选法

法五:HSV颜色空间H范围筛选法

法六:opencv自带肤色检测类AdaptiveSkinDetector


 


常见操作:

图片复制

  • 方法1、重载运算符=

使用重载运算符“=”进行的拷贝是一种浅拷贝,虽然它们有不同的矩阵头,但是二者共享相同的内存空间,二者内容相互关联,任何一个变量变化的同时另一个变量也随之改变。

/*OpenCV v1版本*/
IplImage img_origin = cvLoadImage(".\\picture.jpg", CV_LOAD_IMAGE_COLOR); // 读取一张彩色图 
IplImage img_copy = img_origin;  // 直接赋值,浅拷贝

/*OpenCV v2之后版本*/
Mat img_origin = imread(picture, IMREAD_COLOR);  // 读取一张彩色图 
Mat img_copy = img_origin; 
  • 方法2、cvCopy

cvCopy的原型是:
void cvCopy( const CvArr* src, CvArr* dst, const CvArr* mask CV_DEFAULT(NULL) );
OpenCV官网关于cvCopy函数的介绍
在使用这个函数之前,必须先用cvCreateImage()一类的函数开辟一段内存,然后传递给dst。cvCopy会把src中的数据复制到dst的内存中。这是一种深拷贝,真正地拷贝了一个新的图像矩阵,此时二者相互之间没有影响,但是如果设置了ROI、COI,copy只会复制ROI、COI区域的内容。

/*OpenCV v1版本*/
IplImage img_origin = cvLoadImage(".\\picture.jpg", CV_LOAD_IMAGE_COLOR); // 读取一张彩色图 
IplImage img_copy = cvCreateImage(Size(img_origin->width, img_origin ->height), img_origin ->depth, img_origin ->nChannels); 
// 开辟一个新的内存空间,图像的大小、深度与颜色通道与原图保持一致
cvCopy(img_origin, img_copy);  // 拷贝图像

/*OpenCV v2之后版本*/
Mat img_origin = imread(picture, IMREAD_COLOR);  // 读取一张彩色图 
Mat img_copy;
img_origin.copyTo(img_copy);  //在拷贝数据前会有一步img_copy.create(this->size , this->type)  
  • 方法3、cvCloneImage

cvCloneImage的原型是:
IplImage* cvCloneImage( const IplImage* image );
OpenCV官网关于cvCloneImage函数的介绍
在使用函数之前,不用开辟内存。该函数会自己开一段内存,然后复制好图像里面的数据,然后返回这段内存中的数据。clone是把所有的都复制过来,不论你是否设置了ROI、COI等影响,clone都会原封不动的克隆过来。用clone复制后,如果源图像在内存中消失,复制的图像也变了,而用copy复制,源图像消失后,复制的图像不变。

IplImage img_origin = cvLoadImage(<span class="hljs-string">".\\picture.jpg"</span>, CV_LOAD_IMAGE_COLOR); <span class="hljs-comment">// 读取一张彩色图 </span>
IplImage img_copy = cvCloneImage(img_origin);

这里先学习OpenCV中的一个函数

void flip(InputArray src, OutputArray dst, int flipCode)
//图像变换函数,第三个参数为1时,表示水平反转,0表示垂直反转,负数表示既有水平又有垂直反转。

为介绍OpenCV中的浅拷贝,我们还是从cv::Mat说起吧。cv::Mat类是用于保存图像以及其他矩阵数据的数据结构。当cv::Mat实例化后,分配内存;当对象离开作用域后,分配的内存自动释放。cv::Mat实现了引用计数以及浅拷贝。引用计数的作用是只有当所有引用内存数据的对象都被析构后,内存才会释放。浅拷贝是指当图像之间进行赋值时,图像数据并未发生复制,而是两个对象都指向同一块内存块。
通过OpenCV中的flip函数验证浅拷贝,具体做法:

先声明一个Mat对象img加载本地图片,并显示;
然后声明一个Mat对象img1,将img浅拷贝到img1;
在img1上垂直翻转图片,注意是在原地进行操作,不创建新的图像;
显示img,注意窗口名称应与之前不相同,观察img的图像内容是否改变。

程序如下:

#include<iostream>
#include<opencv2/core/core.hpp>
#include<opencv2/imgproc/imgproc.hpp>
#include<opencv2/highgui/highgui.hpp>

using namespace std;
using namespace cv;

int main()
{
    Mat img1=imread("test.jpg");  //将任意一张名为test.jpg的图片放置于工程文件夹test中
    Mat img2=img1;                //拷贝方式为浅拷贝
    imshow("First",img1);
    if(!img1.data)
    {
        cout<<"error! The image is not built!"<<endl;
        return -1;
    }
    flip(img2,img2,1);            //注意应在原地进行镜像变换
    imshow("Second",img1);
    waitKey();
    return 0;
}

运行如下,很显然,我们修改img1的内容img发生了改变。
这里写图片描述

深拷贝是指新创建的图像拥有原始图像的崭新拷贝,即拷贝图像和原始图像在内存中存放在不同地方。OpenCV中可以通过下面两种方式实现深拷贝。

1) img.copyTo(img1)
2) img1=img.clone()

通过OpenCV中的flip函数验证深拷贝,具体做法与之前相似,将img深拷贝到img1即可。

 

创建一张全黑的图片

方法一:

cv::Mat skinCrCbHist = cv::Mat::zeros(cv::Size(256, 256), CV_8UC1);

方法二:

cv::Mat cirMask = img.clone();
cirMask.setTo(cv::Scalar::all(0));

 

距离:

获取两条直线的距离

实现思路:

①根据给定点的坐标分别获得两条直线的斜率;

②根据两斜率求得直线夹角的正切值;

③以角度或弧度形式返回直线夹角。

代码:

#include <opencv2\opencv.hpp>
#include <iostream>
#include "FunctionsHalcon.h"
 
 
using namespace std;
using namespace cv;
 
 
 
//【1】根据两直线上两点计算两直线夹角
//当flag=0时返回弧度,当flag!-0时返回角度
float lines_orientation1(CvPoint A1, CvPoint A2, CvPoint B1, CvPoint B2, int flag)
{
	//【1】根据直线上两点计算斜率
	float kLine1 = (A2.y - A1.y)/(A2.x - A1.x);
	float kLine2 = (B2.y - B1.y) / (B2.x - B1.x);
 
	//【2】根据两个斜率计算夹角
        float tan_k = 0;                            //直线夹角正切值
	tan_k = (kLine2 - kLine1) / (1 + kLine2*kLine1);
	float lines_arctan = atan(tan_k);            //反正切获取夹角
 
        //【3】以角度或弧度形式返回夹角
	if (flag == 0)
	{
	        return lines_arctan;
	}
	else      
	{
		return lines_arctan* 180.0 / 3.1415926;;
	}
}
 
int main()
{
	
	CvPoint A1 = cvPoint(0,1);
	CvPoint A2 = cvPoint(1,0);
	CvPoint B1 = cvPoint(0,0);
	CvPoint B2 = cvPoint(1,1);
	float a = lines_orientation1(A1, A2, B1, B2, 1);
	cout << a << endl;
 
	waitKey(0);
	system("pause");
	return 0;
 
}

点到直线的距离

 代码:

//p.s. 直线公式用两点式转换成一般式
 //param p1:线外的点
 //param lp1: 线的起点
 //param lp2: 线的终点
 //return 距离
#include "math.h"
function point2Line(p1, lp1, lp2)
{
    double a, b, c,dis;
    // 化简两点式为一般式
    // 两点式公式为(y - y1)/(x - x1) = (y2 - y1)/ (x2 - x1)
    // 化简为一般式为(y2 - y1)x + (x1 - x2)y + (x2y1 - x1y2) = 0
    // A = y2 - y1
    // B = x1 - x2
    // C = x2y1 - x1y2
    a = lp2.y - lp1.y;
    b = lp1.x - lp2.x;
    c = lp2.x * lp1.y - lp1.x * lp2.y;
    // 距离公式为d = |A*x0 + B*y0 + C|/√(A^2 + B^2)
     dis = abs(a * p1.x + b * p1.y + c) / sqrt(a * a + b * b);
    return d;
};

 

视频输入

思路

 

 

 

取某一区域的图片

思路:

Rect的函数定义为: Rect(_Tp _x, _Tp _y, _Tp _width, _Tp _height);

_Tp _x:表示矩形左上角顶点的x坐标;

_Tp _y:表示矩形左上角顶点的y坐标;

_Tp _width:表示矩形框的宽度 ;

_Tp _height:表示矩形框的高度

代码:

Rect rect(50,20, 200, 50);
Mat ROI = img(rect);

 

显示时固定窗口大小

代码

cvNamedWindow("jieguo",0);
cvResizeWindow("jieguo",1000,720);//固定窗口大小为1000*720
cv::imshow("jieguo",mat);

画线和画圆和画矩形

画线

cv::line(mat,p0,p1,cv::Scalar(0,0,225),1,4 ); 在p0,p1点之间画一条红色线,线的粗细为1,

Scalar(0,0,255);红色
Scalar(0,255,0);绿色
Scalar(255,0,0);蓝色

画圆

OpenCV中circle与rectangle函数显示,只不过rectangle在图像中画矩形,circle在图像中画圆。

void circle(Mat  img, Point center, int radius, Scalar color, int thickness=1, int lineType=8, int shift=0)

img为源图像

center为画圆的圆心坐标

radius为圆的半径

color为设定圆的颜色,规则根据B(蓝)G(绿)R(红)

thickness 如果是正数,表示组成圆的线条的粗细程度。否则,表示圆是否被填充

line_type 线条的类型。默认是8

shift 圆心坐标点和半径值的小数点位数

 

画矩形

rectangle (image3, rec1,Scalar(0, 0, 255), -1, 8, 0)

rectangle( CvArr* img, CvPoint pt1, CvPoint pt2, CvScalar color, int thickness=1, int line_type=8, int shift=0 ); 

img 图像.

pt1 矩形的一个顶点。

pt2 矩形对角线上的另一个顶点

color 线条颜色 (RGB) 或亮度(灰度图像 )(grayscale image)。

thickness 组成矩形的线条的粗细程度。取负值时(如 CV_FILLED)函数绘制填充了色彩的矩形。

line_type 线条的类型。见cvLine的描述

shift 坐标点的小数点位数。

 

画多边形

void cvPolyLine( CvArr* img, CvPoint** pts, int* npts, int contours, int is_closed,
                          CvScalar color, int thickness=1, int line_type=8, int shift=0 );

img       图像。
pts       折线的顶点指针数组。
npts     折线的定点个数数组。也可以认为是pts指针数组的大小
contours   折线的线段数量。
is_closed  指出多边形是否封闭。如果封闭,函数将起始点和结束点连线。
color         折线的颜色。
thickness  线条的粗细程度。
line_type  线段的类型。参见cvLine。
shift          顶点的小数点位数。

 

边缘检测

Canny检测

Canny边缘检测是一种非常流行的边缘检测算法,是John Canny在1986年提出的。它是一个多阶段的算法,即由多个步骤构成。

1.图像降噪
2.计算图像梯度
3.非极大值抑制
4.阈值筛选

我们就事后诸葛亮,分析下这个步骤的缘由。

首先,图像降噪。我们知道梯度算子可以用于增强图像,本质上是通过增强边缘轮廓来实现的,也就是说是可以检测到边缘的。但是,它们受噪声的影响都很大。那么,我们第一步就是想到要先去除噪声,因为噪声就是灰度变化很大的地方,所以容易被识别为伪边缘。

第二步,计算图像梯度,得到可能边缘。我们在前面的关于《图像梯度》文章中有所介绍,计算图像梯度能够得到图像的边缘,因为梯度是灰度变化明显的地方,而边缘也是灰度变化明显的地方。当然这一步只能得到可能的边缘。因为灰度变化的地方可能是边缘,也可能不是边缘。这一步就有了所有可能是边缘的集合。

第三步,非极大值抑制。通常灰度变化的地方都比较集中,将局部范围内的梯度方向上,灰度变化最大的保留下来,其它的不保留,这样可以剔除掉一大部分的点。将有多个像素宽的边缘变成一个单像素宽的边缘。即“胖边缘”变成“瘦边缘”。

第四步,双阈值筛选。通过非极大值抑制后,仍然有很多的可能边缘点,进一步的设置一个双阈值,即低阈值(low),高阈值(high)。灰度变化大于high的,设置为强边缘像素,低于low的,剔除。在low和high之间的设置为弱边缘。进一步判断,如果其领域内有强边缘像素,保留,如果没有,剔除。

在OpenCV中,Canny函数本身应该没有将图像降噪包含在内。因此,实施Canny边缘检测时,需要在Canny函数外面执行图像降噪的过程。

# 读入图像
lenna = cv2.imread("images\\lenna_gauss.png", 0)
# 图像降噪
lenna = cv2.GaussianBlur(lenna, (5, 5), 0)
# Canny边缘检测
canny = cv2.Canny(lenna, 50, 150)
cv2.imshow("canny", canny)
cv2.waitKey()

 

在图片上写文字

    void cv::putText(
        cv::Mat& img, // 待绘制的图像
        const string& text, // 待绘制的文字
        cv::Point origin, // 文本框的左下角
        int fontFace, // 字体 (如cv::FONT_HERSHEY_PLAIN)
        double fontScale, // 尺寸因子,值越大文字越大
        cv::Scalar color, // 线条的颜色(RGB)
        int thickness = 1, // 线条宽度
        int lineType = 8, // 线型(4邻域或8邻域,默认8邻域)
        bool bottomLeftOrigin = false // true='origin at lower left'
    );

 

另外,我们在实际绘制文字之前,还可以使用cv::getTextSize()接口先获取待绘制文本框的大小,以方便放置文本框。具体调用形式如下:

	cv::Size cv::getTextSize(
		const string& text,
		cv::Point origin,
		int fontFace,
		double fontScale,
		int thickness,
		int* baseLine
	);

 

例子:

	//创建空白图用于绘制文字
	cv::Mat image = cv::Mat::zeros(cv::Size(640, 480), CV_8UC3);
	//设置蓝色背景
	image.setTo(cv::Scalar(100, 0, 0));
 
	//设置绘制文本的相关参数
	std::string text = "Hello World!";
	int font_face = cv::FONT_HERSHEY_COMPLEX; 
	double font_scale = 2;
	int thickness = 2;
	int baseline;
	//获取文本框的长宽
	cv::Size text_size = cv::getTextSize(text, font_face, font_scale, thickness, &baseline);
 
	//将文本框居中绘制
	cv::Point origin; 
	origin.x = image.cols / 2 - text_size.width / 2;
	origin.y = image.rows / 2 + text_size.height / 2;
	cv::putText(image, text, origin, font_face, font_scale, cv::Scalar(0, 255, 255), thickness, 8, 0);
 
	//显示绘制解果
	cv::imshow("image", image);
	cv::waitKey(0);
	return 0;

 

读取视频并保留每一帧图片取不同的名字

void ExtractFrames(const string &videoName, const string &imagePath, const string &imagePrefix)
{
    
	VideoCapture cap;
	Mat img;

	// 打开视频
	cap.open(videoName);
	if (!cap.isOpened())
	{
		cout << "Error : could not load video" << endl;
		exit(-1);
	}

	// 取得视频帧数
	size_t count = (size_t)cap.get(CV_CAP_PROP_FRAME_COUNT);
	for (size_t i = 0; i < count; ++i)
	{
		cap >> img;
		string imgName = imagePath + "/" + imagePrefix + to_string(i) + ".jpg";
		// 将当前帧保存
		imwrite(imgName, img);
		cout << "Frames " << i << " ... done" << endl;
	}
}
//按时间格式命名
    time_t now = time(NULL);获取1970.1.1至当前秒数time_t
    struct tm * timeinfo = localtime(&now); //创建TimeDate,并转化为当地时间,
                                            //struct tm * timeinfo = gmtime ( &currTime );   //创建TimeDate,并转化为GM时间,
    char path[60];
    strftime(path, 60, "%Y_%m_%d_%H_%M_%S", timeinfo);
    char strPath[100];
    sprintf(strPath, "%s.avi", path);//将创建文件的命令存入cmdchar中
不同名字:
 char* cstr = new char[120];

    //  sprintf(cstr, "%s%d%s", "E:\\OpenCVWorkSpace\\saveCamAsVideo\\saveCamAsVideo\\savedPic\\Pic", n++, ".jpg");

        sprintf(cstr, "%s%d%s", "E:\\OCR_Recognition\\shipin_capture\\shipin_capture", n++, ".jpg");

        imwrite(cstr, frame);

图像灰度化

将RGB图像转化为灰度格式。Opencv中有现成的转化函数:

cvtColor( image, gray_image, CV_BGR2GRAY );

cvtColor 的参数为:
    源图像 (image) 。
    目标图像 (gray_image),用于保存转换图像。
    附加参数,用于指定转换的类型,例子中使用参数 CV_BGR2GRAY 。

图像二值化

图像二值化是图像分析与处理中最常见最重要的处理手段,二值处理方法也非常多。越精准的方法计算量也越大。

方法一:固定一个阈值

该方法非常简单,对RGB彩色图像灰度化以后,扫描图像的每个像素值,值小于127的将像素值设为0(黑色),值大于等于127的像素值设为255(白色)。该方法的好处是计算量少速度快。缺点更多首先阈值为127没有任何理由可以解释,其次完全不考虑图像的像素分布情况与像素值特征。可以说该方法是史最弱智的二值处理方法一点也不为过。

 

方法二:计算像素的平均值

最常见的二值处理方法是计算像素的平均值K,扫描图像的每个像素值如像素值大于K像素值设为255(白色),值小于等于K像素值设为0(黑色)。该方法相比方法一,阈值的选取稍微有点智商,可以解释。但是使用平均值作为二值化阈值同样有个致命的缺点,可能导致部分对象像素或者背景像素丢失。二值化结果不能真实反映源图像信息。

 

方法三:直方图确定阈值

使用直方图方法来寻找二值化阈值,直方图是图像的重要特质,直方图方法选择二值化阈值主要是发现图像的两个最高的峰,然后在阈值取值在两个峰之间的峰谷最低处。该方法相对前面两种方法而言稍微精准一点点。结果也更让人可以接受。

 

方法四:使用近似一维Means方法寻找二值化阈值

该方法的大致步骤如下:

1.      一个初始化阈值T,可以自己设置或者根据随机方法生成。

2.      根据阈值图每个像素数据P(n,m)分为对象像素数据G1与背景像素数据G2。(n为行,m为列)

3.      G1的平均值是m1, G2的平均值是m2

4.      一个新的阈值T’ = (m1 + m2)/2

5.      回到第二步,用新的阈值继续分像素数据为对象与北京像素数据,继续2~4步,

直到计算出来的新阈值等于上一次阈值。

 

方法五:伯恩森算法

 

 

方法六:大津算法

 

肤色检测

复制文章的链接:https://www.cnblogs.com/skyfsm/p/7868877.html

今天我打算写一篇关于使用opencv做皮肤检测的技术总结。那首先列一些现在主流的皮肤检测的方法都有哪些:

  1. RGB color space
  2. Ycrcb之cr分量+otsu阈值化
  3. YCrCb中133<=Cr<=173 77<=Cb<=127
  4. HSV中 7<H<20 28<S<256 50<V<256
  5. 基于椭圆皮肤模型的皮肤检测
  6. 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来获取~

 

去高光和镜面反射

固定区域去高光和镜面反射

转载:https://blog.csdn.net/hello_yxc/article/details/60776315

如下图片中间存在高光,需要消除高光:

涉及的Opencv API为illuminationChange:

处理后的效果如下:(图片顺序对应src, mask, dst三个参数)

最后处理完的感觉类似拿一块玻璃挡住mask所在的区域。

alpha,beta两个参数共同决定消除高光后图像的模糊程度(范围0~2,0比较清晰,2比较模糊)。

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值