因为实验室假期需要写一篇关于opencv的作业 所以顺便看了一下opencv(版本3.2.0)里面关于高斯模糊的源码
分析函数接口
首先,在下用的是vs版本的opencv,是直接编译好给你静态库(.lib)文件的,所以当我按住ctrl
寻找GaussianBlur
这个函数时 只发现了其在imgproc.hpp
里面提供给你的接口。
大概是下面这个样子的:
该函数将源图像与指定的卷积核进行卷积。并且支持原图像直接进行滤波操作。
可以看到这个函数接口主要由四个部分组成:
CV_EXPORTS_W
继续往上找 可以发现这是一个宏定义 可以发现 这个宏定义是在编译阶段将CV_EXPORTS
替换为了CV_EXPORTS_W
再向上查找 会发现 这里又是一个宏组成的用以定义CV_EXPORTS
的宏组,这一段其实就是类似于if else
的结构,主要的作用机理便是适配各类环境(操作系统) 。从中,我们这里又分成了三个部分进行解析:
#if (defined WIN32 || defined _WIN32 || defined WINCE || defined __CYGWIN__) && defined CVAPI_EXPORTS
可以看到,这里判断了两个表达式的逻辑与((defined WIN32 || defined _WIN32 || defined WINCE || defined __CYGWIN__)
和defined CVAPI_EXPORTS
)
首先,defined
的意义在于寻找后面的字段是否已经被宏定义过。
其次,观察第一个表达式,发现_WIN32
在之前已经被定义过了,通过查阅微软官方宏定义文档发现这个字段作用是基于编译器一个信号,表明是Windows环境下编译及运行的程序。 再查找前后未定义过的字段,继而发现WIN32
是只要包含了 Windows.h,那么 WIN32 常量是肯定定义了的设定,不能用于判断平台环境。 而WINCE
便是判断是否为WINCE环境的程序 __CYGWIN__
如果事先有了解过cygwin这个程序的话,这个应该不难看懂,也是用以判断环境的定义,而cywin则是在Windows环境下用以模拟Unix环境的软件(P.S.还是蛮好用的)
再次,观察第二个表达式,也是查找字段是否定义过,这个查了一下,发现这个宏定义是存在于opencv.dll这个文件里面的。也就是判断是否已经链接了动态库。
这样这一行便很容易懂了,目的便是判断程序环境是否为Windows且已经链接opencv.dll的库用以进行下一步操作。
再提一句,关于这个操作系统(环境)的判定其实还有很多,这里放一个Qt里面关于系统判定的头文件(147行开始)(版本比较老了 可自行再自己qt里查看)
# define CV_EXPORTS __declspec(dllexport)
这一句涉及到的主要是dll函数导入,需要和下面最后三行联合起来看 #define CV_EXPORTS
第二行需要联合上面这句一同分析,首先,这个头文件内部写的都是接口,都是作者自己编写留给别人用以调用的,并且从第一点分析的结果来看,这些接口的实现都是放在opencv.dll文件中的,而第一行的判断便是判断你是否在最需要隐式调用,如果有,那么直接可以利用隐式调用的方法进行函数导出(具体__declspec也有在微软文档里写) 反之,如果判断事先未进行链接,那么使用的时候必然是显式调用(没错,就是配置环境的时候配置的lib文件链接,那就是显式调用了参考此篇文章),既然是显式调用,自然便不需要进行函数导出操作。所以将CV_EXPORTS
字段设置为空。
elif defined __GNUC__ && __GNUC__ >= 4
define CV_EXPORTS __attribute__((visibility("default")))
这两句涉及的主要有两个功能: 1. 判断是否为GCC编译环境 2. 为全局动态库函数设置非隐藏声明 对于第一点,类似于分析第一条,可以知道这句的意思便是检查gcc并测试是否高于4.0.0版本(猜测与c++标准有关)
void
函数返回值,这里返回值为空
GaussianBlur
函数名,这里因为是声明所以是跟源码的函数一样,可以很快找到源码定义。
()
括号里面的是函数参数根据上面的说明注释,便可解读出这个函数所有参数意义:
src
这是输入图像,这个图像可以拥有任意数量的通道,这些通道是独立处理的,但深度应该是CV_8U,CV_16U,CV_16S,CV_32F或CV_64F。
dst
这是输出图像,大小与类型与src相同。
ksize
这是卷积核的大小参数(是数字desu)。其中ksize.width和ksize.height可以不同,但是它们必须是一个正奇数(或者零),并且您不用担心卷积核内部参数问题,卷积核会根据sigma来计算。
sigmaX
X方向的卷积核标准偏差。
sigmaY
Y方向的高斯核标准偏差; 如果sigmaY为零,则设置为等于sigmaX,如果两个sigma都是零,则它们是从ksize.width和ksize.height计算的(详见cv :: getGaussianKernel); 这些语义未来均有可能修改,建议指定所有ksize,sigmaX和sigmaY。
borderType
像素外推模式,请参阅cv :: BorderTypes
sepFilter2D, filter2D, blur, boxFilter, bilateralFilter, medianBlur
。这里显示了函数的相关函数,是观察源码的重要提示
这样,对于函数接口的解析就算是完成了,从中我们可以得出以下几个结论:
直接ctrl寻找到底是不能找到原函数代码的。
找到原函数的同时也需要sepFilter2D, filter2D, blur, boxFilter, bilateralFilter, medianBlur这一些列函数辅助理解。
这个函数在cv命名空间中,名称应该为cv::GaussianBlur
。
所以接下来第一步便是需要在源码中寻找到真正的函数实现。
高斯模糊函数本质是利用高斯滤波器对于给定图像进行平滑操作。平滑操作时什么,是减少噪点。噪点的原因又是什么,是单个像素包含的信息过于独立。如何减少噪点,很简单,只需要让每个像素点包含有周围像素的部分信息就行了呗。选用什么方法,最简单的方法便是——卷积(见下图)。利用一个给定的以某种分布函数构建的二维卷积内核,将中心点对准像素点,进行卷积操作,得到的像素点便包含了周围像素的不完全信息,这样子附近的像素点差异性便会越来越小,当整张图像(或选中部分)卷积完成后,每个像素点便不再过于独立,每个便有了附近像素点的信息,这样字图像便不会看上去那么的“扎眼”,会温润平滑许多,这就是高斯模糊的本质了。
[相信看图大概大家就能明白卷积是个啥东西了 再结合上面语句应该就差不多了]
利用grep命令,很容易便能找到这个函数,找到文件并切到这个函数定义,便可以窥见整个函数的全貌了。
以下是cv::GaussianBlur函数全貌:
分析这个函数之前,首先先要从内部关联的函数看起。
CV_INSTRUMENT_REGION()
这一行末尾没有分号,这样的类似函数的字符段有很大可能是一个宏定义变量,经查找,在private.hpp文件中有以下定义:
///// General instrumentation // General OpenCV region instrumentation macro #define CV_INSTRUMENT_REGION() CV_INSTRUMENT_REGION_META(cv::instr::TYPE_GENERAL, cv::instr::IMPL_PLAIN)
经查找,这个字段主要是关于初始化以及边界类型的判断。
if( borderType != BORDER_CONSTANT && (borderType & BORDER_ISOLATED) != 0 )
int type = _src.type();
Size size = _src.size();
_dst.create( size, type );
/**
根据说明文档中所写的
BORDER_CONSTANT = 0, //!< `iiiiii|abcdefgh|iiiiiii`with some specified `i`
BORDER_REPLICATE = 1, //!< `aaaaaa|abcdefgh|hhhhhhh`
BORDER_REFLECT = 2, //!< `fedcba|abcdefgh|hgfedcb`
BORDER_WRAP = 3, //!< `cdefgh|abcdefgh|abcdefg`
BORDER_REFLECT_101 = 4, //!< `gfedcb|abcdefgh|gfedcba`
BORDER_TRANSPARENT = 5, //!< `uvwxyz|absdefgh|ijklmno`
BORDER_REFLECT101 = BORDER_REFLECT_101, //!< same as BORDER_REFLECT_101
BORDER_DEFAULT = BORDER_REFLECT_101, //!< same as BORDER_REFLECT_101
BORDER_ISOLATED = 16 //!< do not look outside of ROI
BORDER_CONSTANT 需要设置borderValue 指定 ' i ' 值(常数)
BORDER_REPLICATE ,复制边界像素
BORDER_REFLECT ,反射复制边界像素
BORDER_REFLECT_101,以边界为对称轴反射复制像素
*/
/*
这里是说如果边缘扩展不是常数扩展,且在规定图像范围内部,便执行下列操作
*/
if( borderType != BORDER_CONSTANT && (borderType & BORDER_ISOLATED) != 0 )
{
// 如果输入矩阵是一个行向量,则滤波核的高强制为1
// 下面同理
if( size.height == 1 )
ksize.height = 1;
if( size.width == 1 )
ksize.width = 1;
}
// 如果核宽跟核高都是一直接复制输出
if( ksize.width == 1 && ksize.height == 1 )
{
_src.copyTo(_dst);
return;
}
CV_OVX_RUN(true, openvx_gaussianBlur(_src, _dst, ksize, sigma1, sigma2, borderType))
这里很明显是一个被宏定义的字段,通过查找,我们能够找到其定位在modules/core/include/opencv2/core/openvx/ovx_defs.hpp
当中
按每行解析的话就是如下:
// 用于检测基于OpenVX的实现的实用程序宏
#ifdef HAVE_OPENVX
// 如果检测到OPENVX便执行下列语句
// 下面这两句主要作用也是标识
#define IVX_HIDE_INFO_WARNINGS // 隐藏警告信息
#define IVX_USE_OPENCV // 使用opencv的标识
#include "ivx.hpp" // 将hpp文件包含进来
#define CV_OVX_RUN(condition, func, ...) \ // ...会被替换
// 这里为了给外界提供OpenVX使用表示 利用宏定义做成了一个接口
// 而这个接口则是主要为了提供硬件层面加速用的(主要面向对象是嵌入式设计)
if (cv::useOpenVX() && (condition) && func) \
{ \
// __VA_ARGS__是可变参数宏定义 用以替换上面...的内容
return __VA_ARGS__; \
}
#else
// 否则便执行下面这条
#define CV_OVX_RUN(condition, func, ...)
#endif // HAVE_OPENVX
其中useOpenVX()
函数主要是返回一个bool类型的变量用以判断是否使用openVX用以计算。
所以CV_OVX_RUN()
这句话便是对于嵌入式设计进行尝试性的优化操作。
ifdef至endif部分
//若之前有过HAVE_TEGRA_OPTIMIZATION优化选项的定义,则执行宏体中的tegra优化版函数并返回
#ifdef HAVE_TEGRA_OPTIMIZATION
// 拷贝到临时变量(暂不知为什么要拷贝到一个临时变量里面去)
Mat src = _src.getMat();
Mat dst = _dst.getMat();
if(sigma1 == 0 && sigma2 == 0 && tegra::useTegra() && tegra::gaussian(src, dst, ksize, borderType))
return;
#endif
这里也显而易见是尝试tegra优化。
但是为什么拷贝到临时变量里也可以改变原值,这里需要进一步到Mat变量的运算符重载里面进行参考。
inline
Mat& Mat::operator = (const Mat& m)
{
if( this != &m )
{
if( m.u )
CV_XADD(&m.u->refcount, 1);
release(); // 归零初始化
/**
归零后进行基本数据共享
*/
flags = m.flags;
if( dims <= 2 && m.dims <= 2 )
{
dims = m.dims;
rows = m.rows;
cols = m.cols;
step[0] = m.step[0];
step[1] = m.step[1];
}
else
copySize(m);
data = m.data;
datastart = m.datastart; // 共享数据头
dataend = m.dataend; // 共享数据尾
datalimit = m.datalimit;
allocator = m.allocator; // 内存共享
u = m.u;
}
return *this; // 返回当前对象的引用
}
我们可以发现在=
重载的时候仅是进行的浅拷贝操作,意思是只将数据头数据尾进行复制,而所有数据的内存数据都是相通共享的。固仅需要一方变量更改便会联动所有对其进行过浅拷贝的变量变化。
具体可以在在下的博客查看:
https://blog.fivezha.cn/2019/02/25/gaussianbulr-analyze/