【专栏介绍】因为专业需要用到OpenCV来处理图像数据,所以需要学习,搜索了网上的相关资料,整体知识比较零散,花费了较多时间,所以才萌生了将学习过程整理成专栏的形式,希望能帮到后来的人,也方便自己复习。如有错漏欢迎评论或者私信指出,我定当及时更正。
本系列共有上中下三篇,后面有空会再做个小项目,并放出来。
上篇:https://blog.csdn.net/weixin_45703465/article/details/122583084https://blog.csdn.net/weixin_45703465/article/details/122583084
中篇:https://blog.csdn.net/weixin_45703465/article/details/122583774https://blog.csdn.net/weixin_45703465/article/details/122583774
下篇:OpenCV中的颜色、形状、人脸和轮廓检测
在这一节,我们将会了解如何使用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
#include
#include
#include
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);//在此处等待
}
关于图像坐标点的获取,在本教程的第一节已经提到了,可以去看一下。
本节将要使用OpenCV提供的颜色检测方法,对图片中的颜色进行提取,主要使用的函数如下:
//转换颜色空间,便于检测
void cvtColor( InputArray src, OutputArray dst, int code, int dstCn = 0 );
//颜色检测
void inRange(InputArray src, InputArray lowerb,
InputArray upperb, OutputArray dst);
还是通过例程来说明相关参数含义:(关于HSV颜色空间的相关知识,见第一节)
#include
#include
#include
#include
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提供的函数进行,简单的形状检测,并给检测出来的形状添加boundingbox。整个过程的流程如下图所示:
图像的预处理阶段,实质上就是通过灰度处理、高斯模糊、边缘检测然后再加粗边缘得到一个二值化的图像,便于边界检测函数进行检测。我们编写下面的预处理函数:
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);//扩张边缘(增加边缘厚度
预处理前后的图片(部分),如下图所示:
图片预处理之后,我们便可以进行形状检测了,需要使用到以下的函数:
void findContours( InputArray image, OutputArrayOfArrays contours,
OutputArray hierarchy, int mode,
int method);
下面我们一一介绍各个参数的含义:
InputArray image :这需要我们预处理之后得到的二值化图像(8bit单通道图像);
OutputArrayOfArrays contours:这是函数的输出,它是std::vector
OutputArray hierarchy:该变量存储了contours中对应元素的相关拓扑信息,其类型为std::vector< cv::Vec4i > 。Vec4i 即 std::vector
int mode,int method :这里的mode 和 method是指形状检测的模式和方法,OpenCV提供了多种的模式和方法,这里不细讲,详细的可以去官网查一下。
所以,为了使用findContours(),我们先要定义其需要的参数。
vector> contours;
vector 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);
得到的效果如下图所示:
不难发现,图像中的所有形状都被检测出来了,因为这张图像是比较理想情况,如果图像中出现一些噪点(例如图像中右上角的小黑圆圈),很可能也会被检测出来,这是我们不想看到的,这个时候,我们就需要采取一些手段来去除这些噪点,这里提供一种根据形状图像面积过滤小形状的方法:
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个形状
}
}
添加此函数以后,我们可以看到,小黑圆圈没有被表示出来:
为了给检测到的形状添加边界框,我们首先需要检测其角点,检测到角点以后,还可以根据角点的个数来判断具体是什么形状,例如三角形一般是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
图中紫色边为我们用来逼近轮廓得到的近似图形,和圆近似的是等八边形;绿色就是我们的边界框了,但是还差了文字标识,下面我们将了解如何正确添加标识文字。
添加标注,首先需要我们对形状进行正确判断,才能添加标注。前面说过,我们可以根据approxCurve 的顶点个数判断形状,例如三角形是三个顶点,矩形是四个顶点。 但是矩形中长方形和正方形怎么区分呢? 这里我们可以根据得到的边界框的长宽比来判断,长宽比为1的时候,我们就可以认为它是正方形,否则是长方形,但是检测边界框是通过approxCurve来的,肯定有误差存在,我们不能将长宽比设定为不变的 1 ,而应该是一个范围,例如 0.9 - 1.1。
来看整个工程的源码
#include
#include
#include
#include
using namespace std;
using namespace cv;
void getContours(Mat imgDil, Mat img) {
//imgDil是传入的扩张边缘的图像用来查找轮廓,img是要在其上绘制轮廓的图像
vector> contours;//轮廓检测到的轮廓。每个轮廓线存储为一个点的向量
//包含关于映像拓扑的信息 typedef Vec Vec4i;具有4个整数值
vector hierarchy;
//在二值图像中查找轮廓。该函数利用该算法从二值图像中提取轮廓
findContours(imgDil, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
vector> conPoly(contours.size());//定义approxCurve
vector 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提供的分类器来实现简单的人脸检测,实现过程十分简单(有现成模型文件的情况下),直接给出源码:(值得注意的是,我们新引进了objdetect.hpp头文件)
#include
#include
#include
#include//对象检测头文件
#include
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 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教程写的,这是他的主页,https://www.murtazahassan.com/,有能力的朋友可以去看看。