OpenCV(二)掩码 *** 作与平滑(均值,高斯模糊)

OpenCV(二)掩码 *** 作与平滑(均值,高斯模糊),第1张

OpenCV知识总结来到了下一个难度高一点的,掩码 *** 作和模糊效果,这是图像处理里面常见的 *** 作。

如果遇到问题请在这里联系我: https://www.jianshu.com/p/67324fb69074

掩码 *** 作实际上思想上很简单:根据一个掩码矩阵(卷积核)重新计算图像中的每一个像素。掩码矩阵中的值表示近邻像素的值(包括自身像素的值)对新像素的值有多大的影响,从数学上观点看来,就是对掩码矩阵内每一个设置好权重,然后对对应的像素领域内做一个权加平均。

卷积是什么?用一个简单的公式来表示:

本质上,卷积就是这种思想。卷积把万事万物看成一个输入,当万事万物的状态出现了变化,则会通过某种系统产生变化,变成另一种输出状态。而这个系统往往就在数学眼里就叫做卷积。

而在深度学习中,往往每一个卷积核是一个奇数的矩阵,做图像识别的时候会通过这个卷积核做一次过滤,筛选出必要的特征信息。

那么掩码 *** 作在数学上是怎么回事?我们平常运用掩码做什么?在OpenCV中掩码最常见的 *** 作就是增加图片对比度。对比度的概念是什么,在上一节聊过,通俗来讲就是能够增强像素之间的细节。我们可以对对每个像素做如下 *** 作:

可能这幅图,理解起来比较困难。实际上流程如此:

举个例子,就以计算出掩码矩阵之后的E的位置,能看到此时是原图中所有的像素都会取出和掩码矩阵一样大小的矩阵。也就是取出原图的红色那一块的领域,分别对E周边包括自己做了一次加权处理,最后赋值回E中。

并且进行如下的权加公式:

这样就能对原来的矩阵进行掩码处理。但是这么做发现没有,如果我们要对A做掩码处理就会发现掩码矩阵对应到原图的位置不存在。现在处理有两种,一种是不对边缘的像素做掩码处理,另一种是为周边的图像做一个padding处理,这种 *** 作在深度学习的图像处理中很常见,通常设置0像素,或者拷贝对边的边缘像素。

能看到这里处理和卷积处理不太一样,只是为了方便,把这种掩码滤波 *** 作称为一种核,是自相关,并不是去计算卷积。

能看到此时这两张图片的对比度有很明显的区别。经过掩码矩阵之后,会发现原图会更加平滑一点,而掩码 *** 作之后会导致整个图片最亮和最暗之间的差距拉大。

从数学公式上来看,当前像素权重为5,周边的点的权重是-1和0.能够发现会对当前的节点加深,同时把周围的像素值减掉,就增加了每一个像素点和周边像素差值,也就是对比度。

当然在OpenCV中,有这么一个函数filter2D,处理掩码 *** 作。

这里创建一个3*3的核。这个核实际上就是上图的那个。这样传递一个掩码矩阵和图像的深度就完成了掩码 *** 作。

平滑也称为模糊,是一项高频率使用的 *** 作。

平滑的作用有很多,其中一项就是降噪音。平滑处理和掩码 *** 作有点相似,也是需要一个滤波器,我们最常用的滤波器就是线性滤波器。线性滤波处理的输出像素值 是输出像素值 的权加和:

其中,h(k,l)成为核,它仅仅只是一个权加系数。图形上的 *** 作和上面的图相似。不妨把核当成一个滑动窗口,不断的沿着原图像素的行列扫动,扫动的过程中,不断的把路过像素都平滑处理。

这里先介绍均值滤波器,它的核心如下:

这里的参数意思是,src:输入的图像,dst:经过均值模糊之后的输出图像,Size:是指这个滤波器的大小,Point是指整个图像模糊绕着哪个原点为半径的进行处理,传入(-1,-1)就是指图像中心,这样就能模糊整个图像。

其计算原理很简单就是,把核里面的所有权重设置为1,最后全部相加求平均值。最后赋值到原来的像素上。

最有用的滤波器 (尽管不是最快的)。 高斯滤波是将输入数组的每一个像素点与 高斯内核 卷积将卷积和当作输出像素值。

高斯模糊实际上是一个二维的高斯核。回顾一下一维的高斯函数:

那么二维实际上就是,就是在原来的x,y轴的情况下,增加一个z轴的纬度,实际上看起来就像一座山一样。

二维的高斯函数可以表示为:

为了达到达到

其OpenCV的调用方式:

这里的参数意思是,src:输入的图像,dst:经过高斯模糊之后的输出图像,Size:是指这个滤波器的大小。sigmaX和sigmaY分别指代的是高斯模糊x轴上和y轴上的二维高斯函数的变化幅度。

换个形象的话说,用上图举个例子,就是确定这个高斯函数这个山的x方向的陡峭程度以及y轴方向的陡峭程度。

下面就高斯模糊,均值模糊和原图的比对

能看到,高斯模糊比起均值模糊保留了图像中相关的形状信息。

为什么会这样呢?原因很简单。因为在计算高斯模糊之前,会根据当前像素区域中所有的像素点进行一次,核的计算,越往中心的权重越高,权重如同小山一下,因此中心的像素权重像素一高了,虽然模糊但是还是保留了原来的形状。

但是当高斯模糊的矩阵大小和sigmaX,sigmaY相似的时候,整个高斯函数就不像山,而是想平原一样平坦。换句话说,整个高斯核中的权重就会,偏向一,就会导致和均值模糊类似效果。

高斯模糊计算流程:

图像中某一段图形的像素是如下分布,

这个时候高斯模糊需要一个核去对每一个位置做滤波。此时不同于均值模糊,没有固定的核矩阵,而是通过上面这个矩阵,计算出高斯的核,最后再计算变化后的矩阵每一个对应的像素。

虽然原理是这样,但是实际上OpenCV为了迅速,在原生实现的时候,内部判断到核是小于7的大小,设置一套固定的高斯模糊矩阵。

这样直接就结束,不是我文章的风格,作为一个程序员,还是有必要探索一下,为什么OpenCV计算速度会比我们自己手写的快。

为了让源码看的不那么辛苦,先聊聊OpenCV底层的设计思想。首先在OpenCV中,内置了几种计算方案,按照效率高低优先度依次的向后执行。

这种设计可以看成我们的平常开发的拦截器设计。当发现优先度高的计算模式发现可以使用的时候,OpenCV将会使用这种模式下的算法进行运算。

一般来说,OpenCV内置如下四个层级计算方案,按照优先顺序依次为:

能看到按照这个优先级不断的向下查找,找到当前OpenCV最快的计算环境。除了最后一个之外,其他三个都是并发计算。

记住这个流程,我们查看OpenCV的源码就很轻松了。

先来看看filter2D的源码。

果不其然,在filter2D实现的第一步,就开始调用CV_OCL_RUN宏去调用OpenCL的显卡并发计算。

能看到,这里面发送了一个condition和一个方法到OpenCL中运行。但是如果,OpenCV在编译的时候,我们没有打开这个OpenCL的选项,没有OpenCL的环境的时候,它实际上就是一个没什么用处的宏:

当有了OpenCL的环境,这个宏就会替换成这个:

能清晰的看到,此时会判断当前的OpenCL是否还在活跃,活跃的状态,并且条件和方法符合规范,就会通过CV_IMPL_ADD,把方法添加到一个vector向量中,让OpenCL读取执行。

在这里面,OpenCV想要使用OpenCL进行计算,就需要这个Mat的类型是UMat,并且是纬度小于等于2.当不符合这两个条件将不会执行OpenCL。

UMat是专门给OpenCL规范计算而使用的矩阵。里面有很多和Mat相似的方法。

此时可能是多线程处理,因此会添加一个智能锁,去保证数据的正确性。

具体的思路,将不作为重点,这边先看看OpenCV是传入了ocl_filter2D的方法,看看这个方法在OpenCL中的执行流程。

OpenCL会把命令最后发送到显卡处理。

实际上这一步和上面的方法有点相似。本质上都是获取需要模糊的区域,如果是(-1,-1),则取中心点,接着判断当前滤波对边缘的处理(BORDER_ISOLATED 不去获取Point为圆心设置的模糊之外的区域)。

能看到这个枚举已经解释很清楚了,默认的边缘处理是复制二个和倒数第二个填充边缘。

最后进入到hal的filter2D进一步 *** 作。

能看到这里有四种方式:

在情况1中,一般的情况replacementFilter2D返回的是一个没有实现的错误码,第二种情况是Intel的并行计算库,没有任何研究,跳过。我们来看看第三种情况和第四种情况

当然这里面判断能够使用dft的判断首先要当前必须要整张图做滤波处理,其次是不能是(0,0)的点为圆心做滤波。最后要判断当前当前的cpu指令是否支持,支持则允许核的宽 高最高为130以内使用原生实现,否则只支持核的宽 高为50以内使用原生实现。

能看到这里面的核心就是调用crossCorr,处理核以及原图的矩阵(使用了快速傅立叶处理相关性计算)。最后从同add添加到目标Mat中,由于add的delta函数为0,因此就和替代的效果一致。

能看到此时,先初始化一个FilterEngine(线性滤波引擎),接着使用apply调用滤波引擎的执行方法。

我们来看看线性引擎的创建:

实际上在这个过程中通过makePtr创建一个sharedptr的指针指向FilterEngine,其原理和Android的智能指针相似。

这个引擎不是关键关键的是getLinearFilter,这个方法创建了一个线性滤波器的实际 *** 作对象。

我们来看看这个结构体:

能看到这里面会根据次数传进来的目标矩阵和原始矩阵的位深创建不同的滤波 *** 作者。

假设,我们现在原图和目标图都是8位位深的矩阵,我们只需要关注下面这个构造函数。

Fliter2D结构体持有着模糊中心点,核,原/目标矩阵, 可以猜测到实际上正在做 *** 作的就是这个结构体。

在preprocess2DKernel方法中,Fliter2D把核的相关信息存储到coords,coeffs中

可以看到此时会判断当前的核矩阵中type是什么,接着再把矩阵中每一个不为0的位置设置进coords,像素数值设置到_coeffs。此时相当于把核矩阵展开成一个向量。

能看到此时滤波引擎会先调用FilterEngine__start,再调用FilterEngine__proceed执行计算。

实际上在FilterEngine__start中计算的是本次循环,需要计算的边界。

FilterEngine__proceed中才是正式计算,做dst循环,最后把具体 *** 作丢给线性引擎生成的Fliter2D的方法中。

了解这两个东西我们直接抽出核心看看fliter是如何运作:

这种运动目标检测的方法还是很经典的,下面写了一些注释仅作参考,希望对你有所帮助。

#include "stdafx.h"

#include "cv.h"

#include "highgui.h"

#include <time.h>

#include <math.h>

#include <ctype.h>

#include <stdio.h>

#include <string.h>

const double MHI_DURATION = 0.1//定义运动跟踪的最大时间

const double MAX_TIME_DELTA = 0.5

const double MIN_TIME_DELTA = 0.05

const int N = 3//定义数组的维度为3

const int CONTOUR_MAX_AERA = 10//定义的阈值

IplImage **buf = 0

int last = 0

IplImage *mhi = 0

CvFilter filter = CV_GAUSSIAN_5x5//高斯卷积滤波

CvConnectedComp *cur_comp, min_comp//定义连通域 *** 作的存储

CvConnectedComp comp//定义连通域 *** 作的存储

CvMemStorage *storage//定义内存分配

CvPoint pt[4]//定义点的存储

/*****************************

*下面update_mhi函数输入img,输出识别结果dst,阈值diff_threshold

*/

void update_mhi( IplImage* img, IplImage* dst, int diff_threshold )

{

double timestamp = clock()/1.//返回从“开启这个程序进程”到“程序中调用clock()函数”时之间的CPU时钟计时单元

CvSize size = cvSize(img->width,img->height)//获取图像的宽和高

int i, idx1, idx2

IplImage* silh

IplImage* pyr = cvCreateImage( cvSize((size.width &-2)/2, (size.height &-2)/2), 8, 1 )//

CvMemStorage *stor//申请内存

CvSeq *cont//定义保存数据的结构

/*先进行数据的初始化*/

if( !mhi || mhi->width != size.width || mhi->height != size.height )

{

//分配内存 *** 作:如果buf是空值,则分配存储空间

if( buf == 0 )

{

buf = (IplImage**)malloc(N*sizeof(buf[0]))//利用malloc动态分配内存

memset( buf, 0, N*sizeof(buf[0]))//作用是在一段内存块中填充某个给定的值,此处值为0

}

//创建通道为N=3,大小为size的图像存储

for( i = 0i <Ni++ )

{

cvReleaseImage( &buf[i] )//释放buf

buf[i] = cvCreateImage( size, IPL_DEPTH_8U, 1 )//创建buf[i]

cvZero( buf[i] )//初始化为0

}

cvReleaseImage( &mhi )//释放变量mhi

mhi = cvCreateImage( size, IPL_DEPTH_32F, 1 )//创建mhi,大小为size,深度为IPL_DEPTH_32F,1个通道

cvZero( mhi )///初始化为0

}

cvCvtColor( img, buf[last], CV_BGR2GRAY )//将RGB图像img转换成gray灰度图像buf

idx1 = last//将last赋值到idx1

idx2 = (last + 1) % N//计算(last + 1)除以N的余数

last = idx2//将idx2赋值到last

silh = buf[idx2]//将buf[idx2]赋值到silh

//下面计算buf[idx1]与buf[idx2]差的绝对值,输出结果存入silh

cvAbsDiff( buf[idx1], buf[idx2], silh )

//下面对单通道数组silh应用固定阈值 *** 作,阈值为30,阈值化类型为CV_THRESH_BINARY最大值为255

cvThreshold( silh, silh, 30, 255, CV_THRESH_BINARY )

//去掉影像(silh) 以更新运动历史图像为mhi,当前时间为timestamp,运动跟踪的最大时间为MHI_DURATION=0.1

cvUpdateMotionHistory( silh, mhi, timestamp, MHI_DURATION )

//下面对mhi进行线性变换 *** 作,输出结果存入dst:dst(I)=mhi(I)*第二个参数 + 第三个参数

cvCvtScale( mhi, dst, 255./MHI_DURATION,

(MHI_DURATION - timestamp)*255./MHI_DURATION )

cvCvtScale( mhi, dst, 255./MHI_DURATION, 0 )

cvSmooth( dst, dst, CV_MEDIAN, 3, 0, 0, 0 )//对dst进行中值滤波

cvPyrDown( dst, pyr, 7 )//利用卷积滤波器对dst进行下采样

cvDilate( pyr, pyr, 0, 1 )//对图像pyr使用3*3长方形进行膨胀 *** 作

cvPyrUp( pyr, dst, 7 )//利用卷积滤波器对dst进行上采样

stor = cvCreateMemStorage(0)//动态内存存储创建内存块

cont = cvCreateSeq(CV_SEQ_ELTYPE_POINT, sizeof(CvSeq), sizeof(CvPoint) , stor)//创建存储结构

//函数cvFindContours为寻找图像dst的角点,数据存入cont中,其中包含角点的坐标值

cvFindContours( dst, stor, &cont, sizeof(CvContour),

CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE, cvPoint(0,0))

for(contcont = cont->h_next)

{

CvRect r = ((CvContour*)cont)->rect//创建矩形区域

if(r.height * r.width >CONTOUR_MAX_AERA)

{

//下面是在图像Img上绘制红色的矩形框,利用左上点和右下点

cvRectangle( img, cvPoint(r.x,r.y),

cvPoint(r.x + r.width, r.y + r.height),

CV_RGB(255,0,0), 1, CV_AA,0)

}

}

cvReleaseMemStorage(&stor)//释放内存

cvReleaseImage( &pyr )//释放结构体

}

int _tmain(int argc, _TCHAR* argv[])

{

IplImage* motion = 0

CvCapture* capture = 0

capture = cvCaptureFromFile("D://Capture1.avi")//获取视频文件

if( capture )

cvNamedWindow( "视频分析", 1 )//创建窗口

{

for()

{

IplImage* image

if( !cvGrabFrame( capture ))//如果读取视频失败,则退出

break

image = cvRetrieveFrame( capture )//获取图像

if( image )

{

if( !motion )

{

motion = cvCreateImage( cvSize(image->width,image->height), 8, 1 )

cvZero( motion )

motion->origin = image->origin

}

}

update_mhi( image, motion, 60)//运动目标检测,阈值为60

cvShowImage( "视频分析", image )//在窗口中显示图像

if( cvWaitKey(10) >= 0 )

break

}

cvReleaseCapture( &capture )//释放

cvDestroyWindow( "视频分析" )//释放窗口

}

return 0

}


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/yw/11884620.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-19
下一篇 2023-05-19

发表评论

登录后才能评论

评论列表(0条)

保存