OpenCV的琐碎知识

时间:2024-04-10 14:08:51

这篇博客将介绍一些OpenCV的琐碎的概念知识以及容易出现错误的点。可能大家平时看博客感觉OpenCV没什么难的,无非是调用一些库和函数,但是在实际操作过程中很容易出现翻车的现象。好了,废话不多说开始本章的内容

内容安排

  • OpenCV各个变量之间的转换关系
  • 采用OpenCV进行连通域分析的原理以及相关函数
  • OpenCV连通域分析的应用-计算欧拉数(euler)
  • 采用OpenCV进行滤波以及形态学处理的相关原理及函数
  • OpenCV轮廓函数的介绍
  • 参考文献

1. OpenCV各个变量之间的转换关系

第一个章节的概念来自于一个函数的应用,当时想要根据MATLAB上的阈值提取函数,实现一个C++版本的,然后在OpenCV上找到一个CVThreshold的函数,因为这个函数提示需要CvArr* 的变量作为填充进去。但是我之前使用一般都是Mat类型的变量没见过类似的。后来查了第一篇文献才知道,这个新的函数变量是什么。简而言之,OpenCV各个变量之间的关系就是:

CvArr
CvMat
IplImage

也就是IplImage是由CvMat派生;CvMat由CvArr派生。因此可以得出CvArr作为函数的参数,无论是传入CvMat或者IplImage,在函数内部都算是CvMat。

接下来再讲讲Mat与CvMat和IplImage之间的异同。首先这两者都能够显示和代表图像。其次Mat侧重于计算,矩阵计算能力更好;CvMat和IplImage更侧重于图像,OpenCV对这两个变量针对图像的操作(缩放、单通道、图像阈值操作等)做了优化。

然后对三个变量分别进行介绍:

  1. 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类型

  2. CvMat类型

    CvMat类似于向量,在创建基础数据类型,比如二维矩阵:

    CvMat* cvCreatMat(int rows ,int cols , int type);

    其中type 可以是任意预定义数据类型,比如RGB或者其他多通道数据。

  3. 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的琐碎知识

因此我们要求欧拉数就需要分析图像的轮廓结构,然后根据轮廓层次结构计算。借助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结构体的四个值的解释如下:

OpenCV的琐碎知识

有了轮廓的层次信息与每个轮廓的信息之后,然后开始遍历每个轮廓,通过调用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时遇到的,其实不是什么难题。

中值滤波

中值滤波是一种非线性滤波器,常用于消除图像中的椒盐噪声。与低通滤波不同的是,中值滤波有利于保留边缘的尖锐度,但是会洗去均匀介质区域中的纹理。

滤波的原理:

输入图像x(n1,n2)x(n_1,n_2)中,以任意一个像素为中心设置一个确定的领域AAAA的边长为2N+1,(N=0,1,2,3....)2N+1,(N=0,1,2,3....)。将领域内个像素的强度值按大小顺序排列,取位于中间位置的那个值(中值)作为该像素点的输出值,滤波公式:A=x(i,j),y=Medx1,x2,x3,,x2N+1A=x(i,j), y=Med {x_1, x_2, x_3,…,x_{2N+1}}

椒盐噪声

椒盐噪声是由图像传感器,传输信道,解码处理等产生的黑白相间的亮暗点噪声。椒盐噪声是指两种噪声,一种是盐噪声(白色,灰度值=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)

    本质上是先腐蚀再膨胀的过程,公式:dst=open(src,element)=dilate(erode(src,element))dst=open(src,element)=dilate(erode(src,element))​,作用是:用来消除小物体、在纤细点处分离物体、平滑较大物体的边界的同时并不明显改变其面积

  • 闭运算(closing operation)

    本质是先膨胀再腐蚀的过程,公式:dst=close(src,element)=erode(dilate(src,element))dst=close(src,element)=erode(dilate(src,element)).作用是:闭运算能够排除小型黑洞

  • 形态学梯度 (Morphological Gradient)

    形态学梯度为膨胀图与腐蚀图之差,公式:dst=morphgrad(src,element)=dilate(src,element)erode(src,element)dst=morph_grad(src,element)=dilate(src,element)-erode(src,element)

    作用:将团块的边缘突出来,也可以保留物体的边缘轮廓

  • 顶帽(Top Hat)

    本质是原图与开运算的差,公式:dst=tophat(src,element)=srcopen(src,element)dst=tophat(src,element)=src-open(src,element)

    作用:因为开运算是放大裂缝或者局部低亮度区域,因此,从原图中减去开运算之后的图,得到的结果突出了比原图轮廓周围的区域更明亮的区域。,应用于分离比临近点亮一些的斑块,当一幅图具有大幅的背景时候,小微物品具有比较规律的情况,可以使用顶帽计算进行背景提取。

    • 黑帽(Black Hat)

    本质是闭运算的结果与原图像之差,公式:dst=blackhat(src,element)=close(src,element)srcdst=blackhat(src,element)=close(src,element)-src

    黑帽运算效果突出比原图轮廓周围的区域更暗的区域,并且这一操作和选择的核大小有关,所以黑帽运算用来分离比临近点暗一点的斑块。

    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转换(总结而来)

OpenCV轮廓层次分析实现欧拉数计算

opencv 连通域需要的函数解析

OpenCV—中值滤波

【OpenCV入门教程之十一】 形态学图像处理(二):开运算、闭运算、形态学梯度、顶帽、黑帽合辑

OpenCV空洞填充算法

【OpenCV3】图像轮廓查找与绘制——cv::findContours()与cv::drawContours()详解