这篇博客将介绍一些OpenCV的琐碎的概念知识以及容易出现错误的点。可能大家平时看博客感觉OpenCV没什么难的,无非是调用一些库和函数,但是在实际操作过程中很容易出现翻车的现象。好了,废话不多说开始本章的内容
内容安排
- OpenCV各个变量之间的转换关系
- 采用OpenCV进行连通域分析的原理以及相关函数
- OpenCV连通域分析的应用-计算欧拉数(euler)
- 采用OpenCV进行滤波以及形态学处理的相关原理及函数
- OpenCV轮廓函数的介绍
- 参考文献
1. OpenCV各个变量之间的转换关系
第一个章节的概念来自于一个函数的应用,当时想要根据MATLAB上的阈值提取函数,实现一个C++版本的,然后在OpenCV上找到一个CVThreshold
的函数,因为这个函数提示需要CvArr*
的变量作为填充进去。但是我之前使用一般都是Mat类型的变量没见过类似的。后来查了第一篇文献才知道,这个新的函数变量是什么。简而言之,OpenCV各个变量之间的关系就是:
也就是IplImage是由CvMat派生;CvMat由CvArr派生。因此可以得出CvArr作为函数的参数,无论是传入CvMat或者IplImage,在函数内部都算是CvMat。
接下来再讲讲Mat与CvMat和IplImage之间的异同。首先这两者都能够显示和代表图像。其次Mat侧重于计算,矩阵计算能力更好;CvMat和IplImage更侧重于图像,OpenCV对这两个变量针对图像的操作(缩放、单通道、图像阈值操作等)做了优化。
然后对三个变量分别进行介绍:
-
Mat类型
在openCV中,Mat是一个多维的密集数据数组。可以用来处理向量和矩阵、图像、直方图等等常见的多维数据。
Mat有三个比较重要的函数:
Mat mat = imread(const String* filename); 读取图像
imshow(const string frameName, InputArray mat); 显示图像
imwrite (const string& filename, InputArray img); 储存图像
Mat类型比CvMat与IplImage类型具有更强的矩阵计算能力,因此在计算密集型应用中,应当首选Mat类型
-
CvMat类型
CvMat类似于向量,在创建基础数据类型,比如二维矩阵:
CvMat* cvCreatMat(int rows ,int cols , int type);
其中type 可以是任意预定义数据类型,比如RGB或者其他多通道数据。
-
IplImage类型
IplImage类型继承自CvMat类型. IplImage类型较之CvMat多了很多参数,比如
depth和nChannels。
一个重要的不便是对
原点
的定义不清楚,图像来源,编码格式,甚至操作系统都会对原地的选取产生影响。为了弥补这一点,openCV允许用户定义自己的原点设置。取值0表示原点位于图片左上角,1表示左下角。
各个类型的相互转换:
A.Mat -> IplImage:IplImage pImg= IplImage(imgMat);
B.Mat -> CvMat:CvMat cvMat = imgMat;
A.CvMat-> IplImage: IplImage* img = cvCreateImage(cvGetSize(mat),8,1);cvGetImage(matI,img);cvSaveImage("rice1.bmp",img);
B.CvMat->Mat:Mat::Mat(const CvMat* m, bool copyData=false);
A.IplImage -> Mat:CvMat mathdr, *mat = cvGetMat( img, &mathdr );
或者CvMat *mat = cvCreateMat( img->height, img->width, CV_64FC3 ); cvConvert( img, mat );
C.IplImage*-> BYTE* :BYTE* data= img->imageData;
2.采用OpenCV进行连通域分析的原理以及相关函数
将这个的原因是,我之前需要提取图像的特征包含求二值图像的欧拉数。在MATLAB上还是比较好实现的,但是用OpenCV实现会遇到各种各样的麻烦。首先我先介绍一下欧拉数的概念。
欧拉数
欧拉数:在二值图像分析中欧拉数是非常重要的拓扑特征,计算公式:E=N-H
,其中E 表示欧拉数;N表示联通组件的数目;H表示联通组件内部的空洞数量。
因此我们要求欧拉数就需要分析图像的轮廓结构,然后根据轮廓层次结构计算。借助OpenCV中的findContours
分析二值图像的轮廓层次会被保存在Vec4i
的结构体内。其中这个函数的API及其参数解释如下所示,具体在用的时候,大家还需再查查,因为我当时被第二个博客的博主给坑了(虽然他写的OpenCV博客质量还是很高的,但是OpenCV版本用的不对,会很蛋疼)。讲下面这些代码之前先介绍一些基本概念(来自最后一篇参考文献)。
轮廓
轮廓是以某种方式表示图像中的曲线的点的列表,表示一条曲线的方式有很多种。OpenCV中,轮廓是由STL风格的vector<>模板对象表示的,其中vector中的每个元素都编码了曲线上,下一点的位置信息。
void cv::findContours(
InputOutputArray image,
OutputArrayOfArrays contours,
OutputArray hierarchy,
int mode,
int method,
Point offset = Point()
)
image参数表示输入的二值图像
contours表示所有的轮廓信息,每个轮廓是一系列的点集合
hierarchy表示对应的每个轮廓的层次信息,我们就是要用它实现对最大轮廓欧拉数的分析
mode表示寻找轮廓拓扑的方法,如果要寻找完整的层次信息,要选择参数RETR_TREE
method表示轮廓的编码方式,一般选择简单链式编码,参数CHAIN_APPROX_SIMPLE
offset表示是否有位移,一般默认是0
这些参数中最重要的参数是hierarchy参数。其输出是vector<Vec4i>
每个轮廓对应的Vec4i
结构体的四个值的解释如下:
有了轮廓的层次信息与每个轮廓的信息之后,然后开始遍历每个轮廓,通过调用findContours
就能够获得二值图的轮廓层次信息,然后遍历每个轮廓,进行层次遍历,获得每个层子轮廓的总数,最终根据洛克层级不同划分为空洞与连接轮廓数,两者相减得到每个独立外层轮廓的欧拉数。
二值化与轮廓发现的代码
Mat gray,binary;
cvtColor(src,gray,COLOR_BGR2GRAY);
threshold(gray,binary,0,255,THRESH_BINARY|THRESH_OTSU);
vector<Vec4i>hireachy;
vector<vector<Point>>contours;
findContours(binary,contours,hireachy,RETR_TREE,CHAIN_APPROX_SIMPLE,Point());
**注意:**这里有个坑,用OTSU
求二值化阈值的时候,一定要将传入的图像以及最终输出的图像转为CV_8UC1
,不然函数会各种报错。
获取同层轮廓的代码
vector<int>current_layer_holes(vector<Vec4i>layers,int index){
int next =layers[index][0];
vector<int>indexes;
indexes.push_back(index);
while(next>=0){
indexes.push_back(next);
next = layers[next][0];
}
return indexes;
}
3. OpenCV-中值滤波
这个问题也是将MATLAB代码转化为OpenCV时遇到的,其实不是什么难题。
中值滤波
中值滤波是一种非线性滤波器,常用于消除图像中的椒盐噪声。与低通滤波不同的是,中值滤波有利于保留边缘的尖锐度,但是会洗去均匀介质区域中的纹理。
滤波的原理:
输入图像中,以任意一个像素为中心设置一个确定的领域,的边长为。将领域内个像素的强度值按大小顺序排列,取位于中间位置的那个值(中值)作为该像素点的输出值,滤波公式:
椒盐噪声
椒盐噪声是由图像传感器,传输信道,解码处理等产生的黑白相间的亮暗点噪声。椒盐噪声是指两种噪声,一种是盐噪声(白色,灰度值=255),另一种是胡椒噪声(pepper noise,黑色,灰度值=0)。前者是高灰度噪声,后者属于低灰度噪声。一般两种噪声同时出现,呈现在图像上就是黑白杂点。对于彩色图像,则表现为单个像素三通道随机出现255与0.
中值滤波函数
void medianBlur( InputArray src, OutputArray dst,int ksize );
//参数
/*
src — 输入图像
dst — 输出图像, 必须与 src 相同类型
ksize — 内核大小 (只需一个值,因为使用正方形窗口),必须为奇数。
*/
//演示代码
cv::Mat image = imread("f:\\images\\castle.jpg",1);
cv::resize(image,image,cv::Size(),0.3,0.3);
// 增加噪声
salt(image,3000);
pepper(image,3000);
//展示噪声结果
cv::imshow("salt image",image);
//中值滤波
Mat result;
cv::medianBlur(image,result,3);
//展示滤波之后的结果
cv::imshow("nedian filted image",result);
cv::waitKey();
4.OpenCV-形态学处理
这一章的内容也是由MATLAB仿真过来的,主要实现的是形态学变换。形态学变换最基本的两种变换是:腐蚀与膨胀。然后以这两种操作可以发展出多种新的形态学操作:开闭运算、形态学梯度、“顶帽”、“黑帽”等
-
开运算(opening operation)
本质上是先腐蚀再膨胀的过程,公式:,作用是:用来消除小物体、在纤细点处分离物体、平滑较大物体的边界的同时并不明显改变其面积
-
闭运算(closing operation)
本质是先膨胀再腐蚀的过程,公式:.作用是:闭运算能够排除小型黑洞
-
形态学梯度 (Morphological Gradient)
形态学梯度为膨胀图与腐蚀图之差,公式:
作用:将团块的边缘突出来,也可以保留物体的边缘轮廓
-
顶帽(Top Hat)
本质是原图与开运算的差,公式:
作用:因为开运算是放大裂缝或者局部低亮度区域,因此,从原图中减去开运算之后的图,得到的结果突出了比原图轮廓周围的区域更明亮的区域。,应用于分离比临近点亮一些的斑块,当一幅图具有大幅的背景时候,小微物品具有比较规律的情况,可以使用顶帽计算进行背景提取。
- 黑帽(Black Hat)
本质是闭运算的结果与原图像之差,公式:
黑帽运算效果突出比原图轮廓周围的区域更暗的区域,并且这一操作和选择的核大小有关,所以黑帽运算用来分离比临近点暗一点的斑块。
5.OpenCV-空洞填补
同样这个问题也是解决的仿真问题。在MATLAB中采用imfill可以很容易实现空洞填充操作。但是在OpenCV中没有这样的函数。
实现步骤:
- 原图为A,A向外延展1到2个像素,将值填充为背景色(0),标记为B
- 使用floodFill函数将B的大背景填充,填充值为前景色(255),种子点为(0,0)即可(确保(0,0)点位于大背景),标记为C
- 将填充好的图像剪裁为原图像大小(去掉延展区域),标记为D
- 将D取反与A相加得到填充的图像,公式E=A|(~D)
//参考代码 #include "stdafx.h" #include<opencv2/core/core.hpp> #include<opencv2/highgui/highgui.hpp> #include<opencv2/imgproc/igporc.hpp> using namespace std; using namespace cv; void fillHole(const Mat srcBw, Mat &dstBw) { Size m_size = srcBw.size(); Mat temp = Mat::zeros(m_size.height+2,m_size.width+2,srcBw.type()); //延展图像 srcBw.copyTo(Temp(Range(1,m_size.height+1),Range(1,m_size.width+1))); cv::floodFill(Temp,Point(0,0),Scalar(255)); Mat cutImg; //剪裁延展的图像 Temp(Range(1,m_size.height+1),Range(1,m_size.width+1)).copyTo(cutImg); dst = srcBw | (~cutImg); } int main(){ Mat img = cv::imread("23.jpg"); Mat gray; cv::cvtColor(img,gray,CV_RGB2GRAY); Mat bw; cv::threshold(gray,bw,0,255,CV_THRESH_BINARY|CV_THRESH_OTSU); Mat bwFill; fillHole(bw,bwFill); imshow("填充之前",gray); imshow("填充之后",bwFill); waitKey(); return 0; }
参考文献
CvArr、Mat、CvMat、IplImage、BYTE转换(总结而来)