【OpenCV(C++)快速入门】–下篇–OpenCV中的颜色、形状、人脸和轮廓检测

【专栏介绍】因为专业需要用到OpenCV来处理图像数据,所以需要学习,搜索了网上的相关资料,整体知识比较零散,花费了较多时间,所以才萌生了将学习过程整理成专栏的形式,希望能帮到后来的人,也方便自己复习。如有错漏欢迎评论或者私信指出,我定当及时更正。本系列共有上中下三篇,后面有空会再做个小项目,并放出来。上篇:https://blog.csdn.net/weixin_45703465/article/details/122583084https://blog.csdn.net/weixin_45703

【专栏介绍】因为专业需要用到OpenCV来处理图像数据,所以需要学习,搜索了网上的相关资料,整体知识比较零散,花费了较多时间,所以才萌生了将学习过程整理成专栏的形式,希望能帮到后来的人,也方便自己复习。如有错漏欢迎评论或者私信指出,我定当及时更正。

本系列共有上中下三篇,后面有空会再做个小项目,并放出来。

上篇:https://blog.csdn.net/weixin_45703465/article/details/122583084【OpenCV(C++)快速入门】--下篇--OpenCV中的颜色、形状、人脸和轮廓检测https://blog.csdn.net/weixin_45703465/article/details/122583084

中篇:https://blog.csdn.net/weixin_45703465/article/details/122583774【OpenCV(C++)快速入门】--下篇--OpenCV中的颜色、形状、人脸和轮廓检测https://blog.csdn.net/weixin_45703465/article/details/122583774

下篇:OpenCV中的颜色、形状、人脸和轮廓检测

3 OpenCV基础进阶

3.1 图像的透视变换

在这一节,我们将会了解如何使用OpenCV的相关函数,来获得图片透视变换的效果,效果示例,如下图所示:

【OpenCV(C++)快速入门】--下篇--OpenCV中的颜色、形状、人脸和轮廓检测

【OpenCV(C++)快速入门】--下篇--OpenCV中的颜色、形状、人脸和轮廓检测

为了实现这个效果,我们需要用到以下几个函数:

//获得从原图到目标效果的变换矩阵
Mat getPerspectiveTransform(const Point2f src[], const Point2f dst[]);
//进行透视变换
void warpPerspective( InputArray src, OutputArray dst,
                                   InputArray M, Size dsize);

在使用**warpPerspective()**时,我们需要明确 InputArray M , M 可以通过 getPerspectiveTransform() 获得,下面通过例程,说明其使用方法。

#include<opencv2/imgcodecs.hpp>
#include<opencv2/highgui.hpp>
#include<opencv2/imgproc.hpp>
#include<iostream>
using namespace std;
using namespace cv;
float w = 250, h = 350;//图片大小
Mat matrix, imgWarp;
void main() {
	//图片用画图打开,在屏幕左下角会显示点的坐标
	string path = "Resources/cards.jpg";
	Mat img=imread(path);
    //这里的src[4]表示的是需要变换的部分在原图像中的坐标分别是左上、右上、左下和右下角
    //这里的dst[4]表示的是矫正后图像的坐标,顺序同上,w和h表示你需要的宽高
	Point2f src[4] = { {529,142},{771,190},{405,395},{674,457} };
	Point2f dst[4] = { {0.0f,0.0f},{w,0.0f},{0.0f,h},{w,h} };
    //这里根据定义的大小,计算变换矩阵 matrix
	matrix = getPerspectiveTransform(src, dst);
	//利用已经明确的matrix获得透视变换后的图像imgWarp
	warpPerspective(img, imgWarp, matrix, Point(w,h));//这里使用size(w,h)替换Point(w,h)也可以
	//将我们提取的坐标绘制到图像上方便观察是否找对点
	for (int i = 0; i < 4; i++) {
		circle(img, src[i], 10, Scalar(0, 0, 255), FILLED);
	}
	imshow("Image", img);
	imshow("Image Warp", imgWarp);
	waitKey(0);//在此处等待
}

关于图像坐标点的获取,在本教程的第一节已经提到了,可以去看一下。

3.2 颜色检测

本节将要使用OpenCV提供的颜色检测方法,对图片中的颜色进行提取,主要使用的函数如下:

//转换颜色空间,便于检测
void cvtColor( InputArray src, OutputArray dst, int code, int dstCn = 0 );
//颜色检测
void inRange(InputArray src, InputArray lowerb,
                          InputArray upperb, OutputArray dst);

还是通过例程来说明相关参数含义:(关于HSV颜色空间的相关知识,见第一节)

#include<opencv2/imgcodecs.hpp>
#include<opencv2/highgui.hpp>
#include<opencv2/imgproc.hpp>
#include<iostream>
using namespace std;
using namespace cv;
//h s v 分别对应 hue 色调; saturation 饱和度;value 亮度
int hmin = 0, smin = 0, vmin = 0;
int hmax = 179, smax = 168, vmax = 56;
void main() {
	string path = "Resources/1.png";
	Mat img=imread(path);
	Mat imgHSV,outImg;
	cvtColor(img, imgHSV, COLOR_BGR2HSV);//转换图像到HSV空间,在其中查找颜色更加容易
    //定义颜色的上下界,是以一个向量的形式呈现的,每个向量都有hsv三个值
    Scalar lower(hmin, smin, vmin);
    Scalar upper(hmax, smax, vmax);
    //检测在lower 和 upper 之间的颜色,并输出在outImg 上
    inRange(imgHSV, lower, upper, outImg);
    imshow("Image", img);
    imshow("Image HSV", imgHSV);
    imshow("Image mask", outImg);
    waitKey(0);
}

这里hmin smin vmin 以及 hmax smax vmax 都是我们事先给定的一些数值,代表了对应的颜色,由于就算是同一种颜色,在一张照片中,由于光照、阴影等多种因素的影响,其数值也会不同,所以我们想要检测某种颜色时,我们需要使用一个范围来尽可能的检测该种颜色的全部表现。

看完上面的例程,我们不禁疑惑,这里给定的lower 和 upper 中的这些值,怎么确定呢,我们也不能改一次值,运行一次来检测效果吧。当然不用这样,我们可以通过添加Trackbar来动态实时调整其数值,并观察效果。增加Trackbar后的例程如下:

int hmin = 0, smin = 0, vmin = 0;
int hmax = 179, smax = 255, vmax = 255;
void main() {
	string path = "Resources/1.png";
	Mat img=imread(path);
	Mat imgHSV,mask;
	cvtColor(img, imgHSV, COLOR_BGR2HSV);
    //创建一个用于放置跟踪栏的窗口
	namedWindow("Trackbars", (640, 200));//(640,200)是尺寸
	//运行时,把3个min的都移到最小值,把3个max的都移到最大值,然后移动使其保持为白色
    //添加Trackbar
	createTrackbar("Hue Min", "Trackbars", &hmin, 179);
	createTrackbar("Hue Max", "Trackbars", &hmax, 179);
	createTrackbar("Sat Min", "Trackbars", &smin, 255);
	createTrackbar("Sat Max", "Trackbars", &smax, 255);
	createTrackbar("Val Min", "Trackbars", &vmin, 255);
	createTrackbar("Val Max", "Trackbars", &vmax, 255);
    
	while (true) {
		//检查数组元素是否位于其他两个数组的元素之间。
		//imgHSV为输入图像,mask为输出图像
		Scalar lower(hmin, smin, vmin);
		Scalar upper(hmax, smax, vmax);
		inRange(imgHSV, lower, upper, mask);
		imshow("Image", img);
		imshow("Image HSV", imgHSV);
		imshow("Image mask", mask);
		waitKey(1);//延时1ms
	}
}

我们运行上面的程序,可以得到如下的结果:

【OpenCV(C++)快速入门】--下篇--OpenCV中的颜色、形状、人脸和轮廓检测

可以看到,我通过适当的调整数值,讲图片中人物头发大致轮廓提取出来了。

3.3 形状、轮廓检测

这小节,将会通过OpenCV提供的函数进行,简单的形状检测,并给检测出来的形状添加boundingbox。整个过程的流程如下图所示:

【OpenCV(C++)快速入门】--下篇--OpenCV中的颜色、形状、人脸和轮廓检测

3.3.1 图像的预处理

图像的预处理阶段,实质上就是通过灰度处理、高斯模糊、边缘检测然后再加粗边缘得到一个二值化的图像,便于边界检测函数进行检测。我们编写下面的预处理函数:

Mat imgGray, imgBlur, imgCanny, imgDil;
cvtColor(img, imgGray, COLOR_BGR2GRAY);//cvt是convert的缩写,将图像从一种颜色空间转换为另一种颜色空间。
GaussianBlur(imgGray, imgBlur,Size(3,3),3,0);//使用高斯滤波器模糊图像。该函数将源图像与指定的高斯核进行卷积,Size(7,7)是核大小,数字越大越模糊
Canny(imgBlur, imgCanny, 25, 75);//边缘检测,阈值1,2可调,目的:显示更多的边缘
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));//创建一个核,增加Size(只能是奇数)会扩张/侵蚀更多
dilate(imgCanny, imgDil, kernel);//扩张边缘(增加边缘厚度

预处理前后的图片(部分),如下图所示:

【OpenCV(C++)快速入门】--下篇--OpenCV中的颜色、形状、人脸和轮廓检测

3.3.2 形状检测

图片预处理之后,我们便可以进行形状检测了,需要使用到以下的函数:

void findContours( InputArray image, OutputArrayOfArrays contours,
                              OutputArray hierarchy, int mode,
                              int method);

下面我们一一介绍各个参数的含义:

InputArray image :这需要我们预处理之后得到的二值化图像(8bit单通道图像);

OutputArrayOfArrays contours:这是函数的输出,它是std::vector<vector> 类型的,可以看成是一个存储多个向量的组。每一个向量代表一个形状。

OutputArray hierarchy:该变量存储了contours中对应元素的相关拓扑信息,其类型为std::vector< cv::Vec4i > 。Vec4i 即 std::vector<int, 4>类型。

int mode,int method :这里的mode 和 method是指形状检测的模式和方法,OpenCV提供了多种的模式和方法,这里不细讲,详细的可以去官网查一下。

所以,为了使用findContours(),我们先要定义其需要的参数。

vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(imgDil, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
//获得提取的轮廓以后,我们可以通过下面的函数将检测到的轮廓在图上表示出来
//其中 -1 表示把全部检测到的轮廓都输出, 向量Scalar(255, 0, 255) 表示颜色, 2 表示轮廓厚度
drawContours(img, contours, -1, Scalar(255, 0, 255), 2);

得到的效果如下图所示:

【OpenCV(C++)快速入门】--下篇--OpenCV中的颜色、形状、人脸和轮廓检测

不难发现,图像中的所有形状都被检测出来了,因为这张图像是比较理想情况,如果图像中出现一些噪点(例如图像中右上角的小黑圆圈),很可能也会被检测出来,这是我们不想看到的,这个时候,我们就需要采取一些手段来去除这些噪点,这里提供一种根据形状图像面积过滤小形状的方法:

for(int i = 0; i < contours.size(); i++) {
    int area = contourArea(contours[i]); //获取每个形状的面积
    if(area > 1000) {//限制面积为> 1000,当然你也可以限制最大值
        drawContours(img, contours, i, Scalar(255, 0, 255), 2);
        //i表示输出第i个形状
    }
}

添加此函数以后,我们可以看到,小黑圆圈没有被表示出来:

【OpenCV(C++)快速入门】--下篇--OpenCV中的颜色、形状、人脸和轮廓检测

3.3.3 添加Boundingbox

为了给检测到的形状添加边界框,我们首先需要检测其角点,检测到角点以后,还可以根据角点的个数来判断具体是什么形状,例如三角形一般是3个角点,矩形一般是4个角点,这时候聪明的可能要问了,圆形的角点怎么找呢?在这里我们使用的方法是使用多边形去逼近一个形状,使得这个多边形语图形的距离达到给定的界限,然后使用多边形的角点近似为图形的角点。这个时候,对于圆形来说,检测的角点一般在6个以上,即使用6边形近似了圆形。使用了下面的函数:

void approxPolyDP( InputArray curve,
                                OutputArray approxCurve,
                                double epsilon, bool closed );

我们解释下参数含义:

InputArray curve:这里我们输入上面findContours() 得到的contours;

OutputArray approxCurve: 这是检测结果,其数据类型和输入是一样的

double epsilon :这个数字就是我们前面提到的给定的界限,也可以理解为精度;

bool closed :closed = true 说明曲线是封闭的,否则相反

对contours进行近似以后,可以得到 approxCurve, 然后我们需要根据得到的approxCurve 来得到boundingbox,会使用到如下的函数:

Rect boundingRect( InputArray array );

该函数根据输入的array(可以是灰度图或者是上面的std::vector<vector< point >>数据类型),可以返回 cv::Rect 数据类型的边界区域。使用上面的两个函数,并将其输出到图像上,可以得到下面的效果:(源码放在本小节最后一起看)

【OpenCV(C++)快速入门】--下篇--OpenCV中的颜色、形状、人脸和轮廓检测

图中紫色边为我们用来逼近轮廓得到的近似图形,和圆近似的是等八边形;绿色就是我们的边界框了,但是还差了文字标识,下面我们将了解如何正确添加标识文字。

添加标注,首先需要我们对形状进行正确判断,才能添加标注。前面说过,我们可以根据approxCurve 的顶点个数判断形状,例如三角形是三个顶点,矩形是四个顶点。 但是矩形中长方形和正方形怎么区分呢? 这里我们可以根据得到的边界框的长宽比来判断,长宽比为1的时候,我们就可以认为它是正方形,否则是长方形,但是检测边界框是通过approxCurve来的,肯定有误差存在,我们不能将长宽比设定为不变的 1 ,而应该是一个范围,例如 0.9 – 1.1。

来看整个工程的源码

#include<opencv2/imgcodecs.hpp>
#include<opencv2/highgui.hpp>
#include<opencv2/imgproc.hpp>
#include<iostream>
using namespace std;
using namespace cv;

void getContours(Mat imgDil, Mat img) {
	//imgDil是传入的扩张边缘的图像用来查找轮廓,img是要在其上绘制轮廓的图像
	vector<vector<Point>> contours;//轮廓检测到的轮廓。每个轮廓线存储为一个点的向量

	//包含关于映像拓扑的信息  typedef Vec<int, 4> Vec4i;具有4个整数值
	vector<Vec4i> hierarchy;
	//在二值图像中查找轮廓。该函数利用该算法从二值图像中提取轮廓
	findContours(imgDil, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
	vector<vector<Point>> conPoly(contours.size());//定义approxCurve
	vector<Rect> boundRect(contours.size());//定义存储边界框的变量
	for (int i = 0; i < contours.size(); i++) {//遍历检测到的轮廓
		int area = contourArea(contours[i]);
		//cout << area << endl;		
		string objectType;//定义轮廓类型,便于添加文字到边界框
		if (area > 1000) {//轮廓面积>1000才绘制
			//定义 0.02*轮廓周长为给定的界限(精度)
			float peri = arcLength(contours[i], true);
			//以指定的精度近似多边形曲线。第二个参数conPloy[i]存储近似的结果,是输出。
			approxPolyDP(contours[i], conPoly[i], 0.02 * peri, true);
			boundRect[i] = boundingRect(conPoly[i]);//计算边界矩形
			//找近似多边形的角点,三角形有3个角点,矩形/正方形有4个角点,圆形>4个角点
			int objCor = (int)conPoly[i].size();
			//cout << objCor << endl;
			if (objCor == 3) { objectType = "Tri"; }
			else if (objCor == 4) {//四个角点进一步判断是正方形还是长方形
				float aspRatio = (float)boundRect[i].width / (float)boundRect[i].height;//宽高比
				if (aspRatio > 0.95 && aspRatio < 1.05) { objectType = "Square"; }
				else objectType = "Rect";
			}
			//这里 objCor 大于4 就判断为圆形过于绝对了,但是在示例图片是可以的
			else if (objCor > 4) { objectType = "Circle"; }
			//drawContours(img, conPoly, i, Scalar(255, 0, 255), 2);
			//绘制边界矩形
			rectangle(img, boundRect[i].tl(), boundRect[i].br(), Scalar(0, 255, 0), 5);
			//添加标注,boundRect[i].y-5 是为了将文字房子框的上方
			putText(img, objectType, { boundRect[i].x,boundRect[i].y - 5 }/*文字坐标*/, FONT_HERSHEY_PLAIN, 1, Scalar(0, 69, 255), 1);
		}
	}
}

void main() {
	string path = "Resources/shapes.png";
	Mat img = imread(path);
	Mat imgGray, imgBlur, imgCanny, imgDil;
	cvtColor(img, imgGray, COLOR_BGR2GRAY);
	//cvt是convert的缩写,将图像从一种颜色空间转换为另一种颜色空间。
	GaussianBlur(imgGray, imgBlur, Size(3, 3), 3, 0);
	//使用高斯滤波器模糊图像。该函数将源图像与指定的高斯核进行卷积,Size(7,7)是核大小,数字越大越模糊
	Canny(imgBlur, imgCanny, 25, 75);
	//边缘检测,阈值1,2可调,目的:显示更多的边缘
	Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
	//创建一个核,增加Size(只能是奇数)会扩张/侵蚀更多
	dilate(imgCanny, imgDil, kernel);//扩张边缘(增加边缘厚度

	getContours(imgDil, img);//img是在其上绘轮廓的图片

	imshow("Image", img);
	waitKey(0);//增加延时,0表示无穷
}

运行上面程序,可以得到如下效果:
【OpenCV(C++)快速入门】--下篇--OpenCV中的颜色、形状、人脸和轮廓检测

3.4 人脸检测

本小节将利用训练好的模型文件,通过OpenCV提供的分类器来实现简单的人脸检测,实现过程十分简单(有现成模型文件的情况下),直接给出源码:(值得注意的是,我们新引进了objdetect.hpp头文件)

#include<opencv2/imgcodecs.hpp>
#include<opencv2/highgui.hpp>
#include<opencv2/imgproc.hpp>
#include<opencv2/objdetect.hpp>//对象检测头文件
#include<iostream>
using namespace std;
using namespace cv;
void main() {
	string path = "Resources/1.png";
	Mat img=imread(path);
    
    //OpenCV提供的分类器
	CascadeClassifier faceCascade;
    //从文件加载分类器(已经训练好的模型)
	faceCascade.load("Resources/haarcascade_frontalface_default.xml");
	//检测文件是否加载成功
	if (faceCascade.empty()) { cout << "XML file not loaded" << endl; }
	vector<Rect> faces;//定义用于接收检测结果的
    //在输入图像中检测不同大小的对象。检测到的对象将以矩形列表的形式返回。
	faceCascade.detectMultiScale(img/*输入*/, faces/*输出*/, 1.1/*比例因子*/, 3/*最近邻*/); //通过增加最近邻的值可以消除误报,但是过大将会导致漏检测
	
	for (int i = 0; i < faces.size(); i++) {
		rectangle(img, faces[i].tl(),faces[i].br(), Scalar(255, 0, 255), 3);//绘制矩形
	}

	imshow("Image", img);
	waitKey(0);//增加延时,0表示无穷
}

通过运行上面的程序,我们看一下结果:

【OpenCV(C++)快速入门】--下篇--OpenCV中的颜色、形状、人脸和轮廓检测

可以看到,图片中的四个人物的脸都被检测出来了

参考声明:

本篇是基于国外一个大神OpenCV教程写的,这是他的主页,https://www.murtazahassan.com/,有能力的朋友可以去看看。

版权声明:本文为博主山海里啊有星辰原创文章,版权归属原作者,如果侵权,请联系我们删除!

原文链接:https://blog.csdn.net/weixin_45703465/article/details/122588456

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
xiaoxingxing的头像xiaoxingxing管理团队
上一篇 2022年1月19日 下午7:57
下一篇 2022年1月19日 下午8:06

相关推荐