<学习opencv>过滤器和卷积
/*=========================================================================*/ // 过滤器和卷积 /*=========================================================================*/ 过滤器,内核和卷积 滤波器是从某个图像i(x,y)开始,通过计算i中像素的某个函数中每个像素的位置x,y来计算新的图像i′(x,y), 该函数位于同一x,y位置周围的某个小区域中。 定义这个小区域形状以及小区域元素组合方式的模板称为过滤器或内核。 在本章中,我们遇到的许多重要的内核都是线性内核。 这意味着分配给I'中x,y点的值可以表示为i中x,y点周围(通常包括)的加权和。 如果你喜欢方程式,可以写下: 这基本上意味着,对于任何大小的内核(例如,5×5), 我们都应该求和内核的面积,对于每对i,j(表示内核中的一个点), 我们应该添加一个等于某个值k i,j乘以i中像素的值, 该像素的值从x,y由i,j。数组i的大小称为内核支持。 任何可以用这种方式表示的滤波器(即线性核)也被称为卷积, 尽管这个术语在计算机视觉社区中经常被随意使用,以包括在整个图像上应用任何滤波器(线性或其他)。 1/25 x 1 1 1 1 1 1 1 1 1 1 1 4 7 4 1 1 1 1 1 1 1 1 1 1 1 -1 0 1 4 16 26 16 4 1 1 1 1 1 1 1 1 1 1 -2 0 2 7 26 41 26 7 1 1 1 1 1 1 1 1 1 1 -1 0 1 4 16 26 16 4 1 1 1 1 1 1 1 1 1 1 1 4 7 4 1 (A)5*5盒核 归一化5*5 3*3索贝尔核 5*5归一化高斯核 锚点::表明内核与源图像对齐 边界外推和边界条件 当我们在OpenCV中查看图像的处理方式时,会出现一些频率问题,即如何处理边框. 与其他一些图像处理库不同,是过滤操作在OpenCV中(cv::blur(),cv::erode(),cv::dilate()等等)产生相同的大小的输入的输出图像。 为了实现该结果,OpenCV在边界处的图像外部创建“虚拟”像素。 您可以看到这对于像这样的操作是必要的,该操作cv::blur() 将获取某个点的邻域中的所有像素并对它们求平均以确定该点的新值。 自己制作边框 您将使用的大多数库函数都将创建这些函数你的虚拟像素。 在这种情况下,您只需要告诉特定功能您希望如何创建这些像素。 同样,为了了解您的选项意味着什么,最好看看允许您显式创建使用一种方法或另一种方法的“填充”图像的功能。 执行此操作的功能是cv::copyMakeBorder() 。 给定要填充的图像,以及稍大的第二个图像, 可以要求cv::copyMakeBorder()以这种或那种方式填充较大图像中的所有像素。 void cv::copyMakeBorder( cv::InputArray src , //输入图像 cv::OutputArray dst , //结束图像 int top, //顶边填充(像素) int bottom, //底边填充(像素) int left, //左侧填充(像素) int right, //右侧填充(像素) int borderType, //像素外推法 const cv::Scalar& value = cv::Scalar() //用于常量边框 ); 要了解每个选项的详细信息,请考虑每个图像边缘的一个极其放大的部分. 通过检查数字可以看出,一些可用的选项是完全不同的。 第一个选项是常量border(cv::BORDER_CONSTANT)将边框区域中的所有像素设置为某个固定值。该值由value 参数to 设置cv::copyMakeBorder() 。(在图10-2 和10-3中 ,这个值恰好是cv::Scalar(0,0,0) 。)下一个选项是环绕(cv::BORDER_WRAP ),为距 图像边缘的距离为n的每个像素分配像素的值。距离 对边的距离为n。复制选项cv::BORDER_REPLICATE 将边缘上的每个像素指定为与该边缘上的像素相同的值。 最后,有两种略有不同的反射形式:cv::BORDER_REFLECT和cv::BORDER_REFLECT_101 。 第一个指定距离为n的每个像素离开图像边缘的像素值是距离相同边缘的距离n in。 相反,cv::BORDER_REFLECT_101 将距离n的每个像素从图像的边缘分配距离 相同边缘的距离为n + 1 的像素的值(结果是不复制非常边缘的像素)。 在大多数情况下,cv::BORDER_REFLECT_101 是OpenCV方法的默认行为。 cv::BORDER_DEFAULT 解决的变量cv::BORDER_REFLECT_101 。 表总结了这些选项。 ------------------------------------------------- 边框类型 | 影响 ------------------------------------------------- cv::BORDER_CONSTANT | 使用提供的(常量)值扩展像素 ------------------------------------------------- cv::BORDER_WRAP | 通过从相对侧复制来扩展像素 ------------------------------------------------- cv::BORDER_REPLICATE | 通过复制边缘像素来扩展像素 ------------------------------------------------- cv::BORDER_REFILECT | 通过反射扩展像素 ------------------------------------------------- cv::BORDER_REFILECT | 通过反射扩展像素 ------------------------------------------------- cv::BORDER_REFILECT | 通过反射扩展像素 ------------------------------------------------- cv::BORDER_REFLECT_101 | 通过反射扩展像素,边缘像素不会加倍 ------------------------------------------------- cv::BOEDER_DEFAULT | 别名cv::BORDER_REFLECT_101 ------------------------------------------------- 手动外推 在某些情况下,你需要计算引用特定的边缘像素的参考像素的位置. 例如,给定宽度为w 和高度为h的图像, 您可能想知道该图像中的哪个像素用于为虚拟像素分配值(w + dx,h + dy )。 虽然这个操作基本上是外推,但是为你计算这样一个结果的函数被称为cv::borderInterpolate() : int cv::borderInterpolate( int p, //0基于外推像素的坐标 int len, //数组长度(在相关那个) int borderType //像素外推法 ) 该cv::borderInterpolate() 函数一次计算一维的外推。 它需要一个坐标p ,一个长度len (相关方向上的图像的实际大小)和一个borderType 值。 因此,例如,您可以在一个混合边界条件下计算图像中特定像素的值, BORDER_REFLECT_101 在一个维度中使用,在另一个维度中使用BORDER_WRAP : float val = img.vat<float>( cv::borderInterpolate(100, img.rows, BORDER_REFLECT_101) CV::borderInterpolate(-5, img.cols, BORDER_WRAP) ); 门槛操作 OpenCV功能cv::threshold()完成,这些任务(见调查[Sezgin04 ])。 基本思想是给出一个数组,连同一个阈值,然后根据数组的每个元素是低于还是高于阈值, 发生某些事情。如果您愿意,可以将阈值视为一个非常简单的卷积 使用1×1内核然后对该像素执行几个非线性操作之一的操作: double cv::threshold( cv::InputArray src //输入图像 cv::OutputArray dst //结果图像 double thresh //阈值 double maxValue //向上操作的最大值 int thresholdType //要使用的阈值类型 ) cv :: threshold()的thresholdType选项 门槛类型 手术 ----------------------------------------------------- cv::THRESH_BINARY DST I =(SRC I > thresh )?MAXVALUE:0 ----------------------------------------------------- cv::THRESH_BINARY_INV DST I =(SRC I > thresh )?0:MAXVALUE ----------------------------------------------------- cv::THRESH_TRUNC DST I =(SRC I > thresh )?THRESH:SRC I ----------------------------------------------------- cv::THRESH_TOZERO DST I =(SRC I > thresh )?SRC I :0 ----------------------------------------------------- cv::THRESH_TOZERO_INV DST I =(SRC I > thresh )?0:SRC I ----------------------------------------------------- 使用cv::threshold()对图像的三个通道求和 #include <opencv2/opencv.hpp> #include <iostream> using namespace std; void sum_rgb(const cv::Mat& src, cv::Mat& dst){ // 将图像分割到颜色平面上 vector<cv::Mat> planes; cv::split(src,planes); cv::Mat b = planes[0], g = planes[1], r = planes[2],s; //添加相等加权的rgb值 cv::addWeighted(r, 1./3, g, 1./3, 0.0, s); cv::addWeighted(s, 1., b, 1./3, 0.0, s); //截断值大于100值 cv::threshold(s, dst, 100, 100, cv::THRESH_TRUNC); } int main(){ cv::Mat src = cv::imread( argv[1] ), dst; if( src.empty() ) { cout << "can not load " << argv[1] << endl; return -1; } sum_rgb( src, dst); //创建一个带有文件名称的命名窗口 //在窗口中显示图像 cv::imshow(argv[1],dst); //闲置直到用户点击任何键 cv::waitKey(0); return 0; } 组合和阈值图像平面的替代方法 void sum_rgb(const cv::Mat&src, cv::Mat&dst){ //将图像分割到颜色平面上 vector<cv::Mat> planes; cv::split(src, planes); cv::Mat b = plane[0]; cv::Mat g = plane[1]; cv::Mat r = plane[2]; //累积单独的平面,组合和阈值 cv::Mat s = cv::Mat::zeros(b.size(), CV_32F); cv::accumulate(b,s); cv::accumulate(g,s); cv::accumulate(r,s); //截断大于100的值并重新调整为dst cv::threshold(s,s,100,100,cv::THRESH_TRUNC); s.convertTo(dst,b.type()); } 自适应阈值 存在修改的阈值技术,其中阈值水平本身是可变的(跨图像). 在Opencv中,这种方法是实施在cv::adaptiveThreshold()函数中:: void cv::adaptiveThreshold( cv::InputArray, //输入图像 cv::OutputArray, //结果图像 double maxValue, //向上操作的最大值 int adaptiveMethod, //mean或Gaussian int thresholdType //要使用的阈值类型 int blockSize, //块大小 double C //常数 ) cv::adaptive threshold()允许两种不同的自适应阈值类型,具体取决于adaptivemethod的设置。 在这两种情况下,我们通过计算每个像素位置周围的b×b区域的加权平均值减去一个常数来设置自适应阈值t(x,y), 其中b由块大小给出,常数由c给出。如果该方法设置为cv::adaptive_thresh_mean_c, 则该区域中的所有像素的权重相等。如果将其设置为cv::adaptive_thresh_gaussian_c, 则根据距离该中心点的高斯函数对(x,y)周围区域中的像素进行加权 当存在相对于一般强度梯度需要阈值的强照明或反射梯度时,自适应阈值技术非常有用。 此功能仅处理单通道8位或浮点图像,并且要求源图像和目标图像不同。 阈值与自适应阈值 #include <iostream> using namespace std; int main( int argc, char** argv ) { if(argc != 7) { cout << "用法": " <<argv[0] <<" fixed_threshold invert(0=off|1=on) " "adaptive_type(0=mean|1=gaussian) block_size offset image\n" "例子: " <<argv[0] <<" 100 1 0 15 10 fruits.jpg\n"; return -1; } double fixed_threshold = (double)atof(argv[1]); int threshold_type = atoi(argv[2]) ? cv::THRESH_BINARY : cv::THRESH_BINARY_INV; int adaptive_method = atoi(argv[3]) ? cv::ADAPTIVE_THRESH_MEAN_C : cv::ADAPTIVE_THRESH_GAUSSIAN_C; int block_size = atoi(argv[4]); double offset = (double)atof(argv[5]); cv::Mat Igray = cv::imread(argv[6], cv::LOAD_IMAGE_GRAYSCALE); // 读入灰色图像 // if( Igray.empty() ){ cout << "Can not load " << argv[6] << endl; return -1; } //声明灰色图像 // cv::Mat It, Iat; // 阈值 // cv::threshold( Igray, It, fixed_threshold, 255, threshold_type); cv::adaptiveThreshold( Igray, Iat, 255, adaptive_method, threshold_type, block_size, offset ); // 显示结果 // cv::imshow("Raw",Igray); cv::imshow("Threshold",It); cv::imshow("Adaptive Threshold",Iat); cv::waitKey(0); return 0; } [[平滑]] 平滑,也称为模糊,是一个简单且经常的使用图像处理操作. 平滑有很多作用,但是通常都是为了减少噪音或相机伪影. 当我们希望以原则性的方式降低图像的分辨率时,平滑也很重要. opencv提供了五种不同的平滑操作,每种操作都有自己的关联库函数, 每种操作完成的平滑类型都略有不同。所有这些函数中的SRC和DST参数都是常见的源和目标数组。 之后,每个平滑操作都有特定于关联操作的参数。其中,唯一常见的参数是最后一个borderType。 这个参数告诉平滑操作如何处理图像边缘的像素。 简单的模糊和盒子过滤器 void cv::blur( cv::InputArray src, //输入图像 cv::OutputArray dst, //结果图像 cv::Size ksize, //内核大小 cv::Point anchor = cv::Point(-), //瞄点位置 int borderType = cv::BOEDER_DEFAULT //要使用的边界外推法 ) 简单的模糊操作由cv::blur()提供。 输出中的每个像素是窗口(即内核)中所有像素的简单平均值,围绕输入中相应的像素。 此窗口的大小由参数ksize指定。参数定位点可用于指定内核如何与正在计算的像素对齐。 默认情况下,anchor的值是cv::point(-1,-1),这表示内核应该相对于过滤器居中。 对于多通道图像,每个通道将单独计算。 简单模糊是Box过滤器的一个专门版本。 框式过滤器是任何具有矩形轮廓且其值k i、j均相等的过滤器。 在大多数情况下,k i,j=1代表所有i,j,或k i,j=1/a,其中a是过滤器的面积。 后一种情况称为归一化盒滤波器 void cv::boxFilter( cv::InputArray src, //输入图像 cv::OutputArray dst, //结果图像 int ddepth, //输出深度 cv::Size ksize, //内核大小 cv::Point anchor //瞄点的位置 bool normalize = true //如果是true,则除以框区域 int borderType = cv::BOEDER_DEFAULT //要使用的边界外推法 ) opencv函数cv::boxfilter()是一种更一般的形式,cv::blur()本质上是一种特殊情况。 cv::boxfilter()和cv::blur()的主要区别: 在于前者可以在非规范化模式下运行(normalize=false),并且可以控制输出图像dst的深度。 (在cv::blur()的情况下,dst的深度将始终等于src的深度) 如果将ddepth的值设置为-1,则目标图像的深度将与源图像的深度相同; 否则,您可以使用任何常用别名(例如cv f)。 1/25 x 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 中值滤波 中值滤波器用围绕中心像素的矩形邻域中的中值或“中值”像素(相对于平均像素)替换每个像素。 通过平均进行简单的模糊可以对有噪声的图像敏感,尤其是具有较大孤立异常值的图像(例如,数码摄影中的镜头噪声)。 即使是少量点的大差异也会导致平均值的显著移动。 中值滤波可以通过选择中间点来忽略异常值。 void cv::medianBlur( cv::InputArray src , //输入图像 cv::OutputArray dst, //输出图像 cv::Size ksize //内核大小 ) cv::medianblur的参数:源数组src、目标数组dst和内核大小ksize。 对于cv::medianblur(),始终假定定位点位于内核的中心。 高斯滤波器 高斯滤波器,可能是最有用的。 高斯滤波包括将输入数组中的每个点卷积为(标准化)高斯核,然后求和以生成输出数组: void cv::GaussianBlur( cv::InputArray src, //输入图像 cv::OutputArray dst, //结果图像 cv::Size ksize, //内核大小 double sigmaX, //x方向的高斯半宽 double sigmaY, //y方向的高斯半宽 int borderType = cv::BOEDER_DEFAULT //要使用的边界外推法 ) 参数ksize 给出了滤镜窗口的宽度和高度。 下一个参数表示高维内核在x维度中的西格玛值(半高半宽)。 第四个参数类似地表示y维度中的西格玛值。 如果仅指定x 值,并将y 值设置为0(其默认值),则y和x值将相等。 高斯平滑的OpenCV实现还为几个常见内核提供了更高的性能优化。 具有“标准”sigma(即sigmaX = 0.0 )的3×3,5×5和7×7内核比其他内核具有更好的性能。 高斯模糊支持8位或32位浮点格式的单通道或三通道图像,并且可以在适当的位置完成。 双边滤波器 void cv::bilateralFilter( cv::InputArray src, //输入图像 cv::OutputArray dst, //结果图像 int d, //像素领域大小(最大距离) double sigmaColor, //颜色权重函数的宽度参数 double sigmaSpace, //空间权重函数的宽度参数 int borderType = cv::BOEDER_DEFAULT //要使用的边界外推法 ) 双边滤波是一种较大的图像分析算子,称为保边平滑。 与高斯平滑相比,双边滤波最容易理解。 高斯平滑的一个典型动机是,真实图像中的像素应该在空间上缓慢变化, 从而与相邻的像素相关联,而随机噪声在每个像素之间可以有很大的变化(即,噪声没有空间相关性)。 正是在这个意义上,高斯平滑在保持信号的同时减少了噪声。 不幸的是,该方法在边缘附近分解,在那里您确实希望像素与边缘上的相邻像素不相关。 因此,高斯平滑会模糊边缘。不幸的是,以处理时间大大延长为代价, 双边滤波提供了一种平滑图像而不平滑边缘的方法。 与高斯平滑一样,双边滤波构造每个像素及其相邻分量的加权平均值。 加权有两个分量,第一个是高斯平滑使用的相同加权。第二分量也是高斯加权, 但不基于距中心像素的空间距离,而是基于距中心像素的强度差 。 您可以将双边滤波视为高斯平滑,它比相似的像素更重要,同时保持高对比度边缘清晰。 此滤镜的效果通常是将图像转换为同一场景的水彩画.这可以用来帮助分割图像。 双边过滤需要三个参数(源和目的地除外)。第一个是d 在过滤期间考虑的像素邻域的直径。 第二个是被称为颜色域的高斯核的宽度sigmaColor ,它类似于高斯滤波器中的西格玛参数。 第三个是空间域中高斯核的宽度sigmaSpace 。第二个参数越大, 将包括在平滑中的强度(或颜色)的范围越宽(因此,为了保持不连续性,必须更加极端)。 过滤器大小d 对算法的速度有很强的影响(正如您所料)。 视频处理的典型值小于或等于,但可能与9 非实时应用程序一样高。 作为d显式指定的替代方法,您可以将其设置为-1 ,在这种情况下,它将自动计算sigmaSpace 。 导数和梯度 最基本和最重要的卷积之一是计算导数(或它们的近似值)。 有许多方法可以做到这一点,但只有少数方法非常适合特定的情况。 索贝尔导数(Sobel) void cv::Sobel( cv::InputArray src, //输入图像 cv::OutputArray dst, //结果图像 int ddepth, //输出的像素的深度 int xorder, //x中相应导数的顺序 int yorder, //y中相应导数的顺序 cv::Size ksize = 3, //内核大小 double scale = 1, //Scale(在分配前应用) double delate = 0, //Offset(在赋值前应用) int borderType = cv::BOEDER_DEFAULT //边界外推 ) 这里,SRC和DST是您的图像输入和输出。 参数ddepth允许您选择生成输出的深度(类型)(例如,cv f)。 作为如何使用DDEPth的一个好例子,如果SRC是一个8位图像, 那么DST的深度应该至少为cv s,以避免溢出。 xorder和yorder是导数的阶。通常,您将使用0、1或最多2;0值表示该方向没有导数。 ksize参数应该是奇数,是要使用的过滤器的宽度(和高度)。 目前,光圈大小最多支持31个。12在DST中存储之前,将比例因子和增量应用于导数。 当您想要在屏幕上显示的8位图像中真实地显示导数时,这一点非常有用: 该borderType 参数的功能与其他卷积操作的描述完全相同 。 Sobel运算符具有良好的特性,可以为任何大小的内核定义它们,并且可以快速迭代地构造这些内核。 较大的谷粒对噪声的敏感度较低,因此可以更好地逼近导数。 然而,如果导数不希望在空间上是常数,那么显然,一个太大的内核将不再给出一个有用的结果。 为了更准确地理解这一点,我们必须认识到索贝尔算符并不是在离散空间上定义的导数。 Sobel运算符实际表示的是对多项式的拟合。 也就是说,x方向的二阶索贝尔算符不是真正的二阶导数,它是对抛物函数的局部拟合。 这就解释了为什么人们可能想要使用一个更大的内核: 更大的内核正在计算一个更大像素数上的匹配度。 Scharr过滤器 事实上,在离散网格的情况下,有很多方法可以近似导数。 对于sobel运算符,使用的近似值的缺点是它对于小内核的精度较低。 对于较大的内核,在近似中使用了更多的点,这个问题就不那么重要了。 在cv::sobel()中使用的x和y过滤器不会直接显示这种不准确,因为它们与x轴和y轴精确对齐。 当您想对图像进行近似于方向导数的测量时 (即,使用两个方向滤波器响应之比y/x的反正切来测量图像梯度的方向),就会遇到困难。 为了说明这一点,您可能需要在何处进行此类图像测量的具体示例是, 通过组装对象周围的渐变角度柱状图,从对象收集形状信息的过程。 这种直方图是许多常用形状分类器训练和操作的基础。 在这种情况下,不准确的梯度角测量会降低分类器的识别性能。 对于一个3×3的索贝尔滤波器,其误差越明显,梯度角与水平或垂直方向的距离越远。 opencv通过在cv::sobel()函数中使用特殊的ksize值cv::scharr, 解决了小型(但快速)3×3 sobel导数过滤器的这种不精确性。 Scharr滤波器和Sobel滤波器一样快但更精确, 因此如果您想使用3×3滤波器进行图像测量,应该始终使用它。 -3 0 +3 -3 -10 -3 -10 0 +10 0 0 0 -3 0 +3 +3 +10 +3 拉普拉斯函数The Laplacian 因为 拉普拉斯算子可以用二阶导数来定义,你可能会认为离散实现的工作类似于二阶索贝尔导数。 确实如此,事实上,拉普拉斯算子的OpenCV实现在其计算中直接使用Sobel算子: void cv::Laplacian( cv::InputArray src, //输入图像 cv::OutputArray dst, //结果图像 int ddepth, //输出图像的深度的深度(CV_8U) cv::Size ksize, //内核大小 double scale = 1, //在分配给dst之前应用缩放 double delta = 0, //在分配给dst之前应用了偏移量 int borderType = cv::BOEDER_DEFAULT //边界外推 ) 该cv::Laplacian() 函数采用与函数相同的参数,但cv::Sobel() 不需要导数的顺序。 这个孔径ksize 是精确的 与Sobel衍生物中出现的孔径相同, 并且实际上,给出了在二阶导数的计算中对像素进行采样的区域的大小。 在实际实现中,对于ksize 除此之外的任何事物,拉普拉斯算子直接从相应的Sobel算子的总和计算。 在特殊情况下ksize=1 ,拉普拉斯算子通过卷积计算 0 1 0 1 -4 1 0 1 0 拉普拉斯算子可以在各种上下文中使用。常见的应用是检测“斑点”。 回想一下拉普拉斯算子的形式是沿x轴和y轴的二阶导数之和。 这意味着由较高值包围的单个点或任何小斑点(小于光圈)将倾向于最大化此功能。 相反,由较低值包围的点或小斑点将倾向于最大化该函数的负值。 考虑到这一点,拉普拉斯算子也可以用作一种边缘检测器。 要了解如何完成此操作,请考虑函数的一阶导数,当函数快速变化时,它会(当然)很大。 同样重要的是,随着我们接近边缘状的不连续性,它会迅速增长,并且当我们越过不连续点时会迅速收缩。 因此,导数将在该范围内的某个局部最大值处。 因此,我们可以查看二阶导数的0来表示这种局部最大值的位置。 原始图像中的边缘将是拉普拉斯算子的0。 不幸的是,实质性和不太有意义的边缘都是拉普拉斯算子的0, 但这不是问题,因为我们可以简单地滤出那些也具有较大的第一(Sobel)导数值的像素。 图像形态学处理 基本的形态转换被称为扩张和侵蚀,它们出现在各种各样的环境中, 例如消除噪音、隔离单个元素以及在图像中连接不同的元素。 基于这两个基本操作的更复杂的形态学操作,也可以用来发现图像中的强度峰值(或孔), 并定义(还有另一种)图像梯度的特殊形式。 膨胀是某些图像与内核的卷积,其中任何给定的像素都被内核所覆盖的所有像素值的局部最大值替换。 正如我们前面提到的,这是一个非线性操作的例子,因此内核不能用图10-1所示的形式表示。 通常,用于膨胀的核是一个“实心”正方形内核,有时是磁盘,锚定点在中心。 形态膨胀:在平方核下取最大值 侵蚀是相反的操作。侵蚀算子的作用相当于计算内核区域的局部最小值。 形态侵蚀:在方形核心下取最小值 图像形态通常在布尔图像上完成,这是由阈值操作产生的。 然而,因为扩张仅仅是最大操作者并且侵蚀仅是最小操作者,所以形态也可以用于强度图像。 一般来说,膨胀会扩大明亮区域,而侵蚀则会减少明亮区域。 此外,膨胀会填满凹坑,侵蚀会去除突出物。 当然,确切的结果将取决于内核,但只要内核是凸的和填充的,这些语句通常是正确的。 void cv::erode( cv::InputArray src, //输入图像 cv::OutputArray dst, //结果图像 cv::InputArray element, //结构化 cv::Point anchor = cv::Point(-1,-1), //瞄点的位置 int iterations = 1, //应用的次数 int borderType cv::BORDER_CONSTANT, //边界外推 const cv::Scalar&borderValue = cv::morphologyDefaultBorderValue() ) void cv::dilate( cv::InputArray src, //输入图像 cv::OutputArray dst, //结果图像 cv::InputArray element, //结构化 cv::Point anchor = cv::Point(-1,-1), //瞄点的位置 int iterations = 1, //应用的次数 int borderType cv::BORDER_CONSTANT, //边界外推 const cv :: Scalar&borderValue = cv :: morphologyDefaultBorderValue() ) cv::corrude()和cv::explate()都采用源映像和目标映像, 并且都支持“就地”调用(其中源映像和目标映像相同)。 第三个参数是kernel,您可以将一个未初始化的数组cv::mat()传递给它,这将使它默认使用一个3×3内核, 并将锚定在其中心(稍后我们将讨论如何创建您自己的内核)。第四个参数是迭代次数。 如果未设置为默认值1,则该操作将在对函数的单个调用期间应用多次。 borderType参数是常见的边框类型, borderValue是当borderType设置为cv::border_常量时将用于边缘外像素的值。 腐蚀操作通常用于消除图像中的“斑点”噪声。这里的想法是斑点被腐蚀成什么都没有, 而包含视觉显著内容的较大区域不受影响。 “膨胀”操作通常用于查找连接的组件(即具有类似像素颜色或强度的大型离散区域)。 膨胀的效用产生是因为在许多情况下,由于噪声、阴影或其他类似的效果, 一个大的区域可能被分解成多个组件。小的膨胀会使这些成分“熔化”成一个整体。 一般形态学功能 当您使用布尔图像和 像素处于开启(> 0)或关闭(= 0)的图像掩模,基本的侵蚀和扩张操作通常就足够了。 但是,当您使用灰度或彩色图像时,许多其他操作通常会有所帮助。 几个更有用的操作 可以通过多功能cv::morphologyEx() 函数处理。 void cv :: morphologyEx( cv :: InputArray src,//输入图像 cv :: OutputArray dst,//结果图像 int op,//运算符(例如cv :: MOP_OPEN) cv :: InputArray元素,//结构元素,cv :: Mat() cv :: Point anchor = cv :: Point(-1,-1),//锚点的位置 int iterations = 1,//要应用的次数 int borderType = cv :: BORDER_DEFAULT //边界外推 const cv :: Scalar&borderValue = cv :: morphologyDefaultBorderValue() ); cv::morphologyEx() 还有一个新的 - 非常重要的 - 参数。这个叫做的新参数op是要完成的具体操作 cv :: morphologyEx()操作选项 --------------------------------------------------- 价值 形态学运算符 需要临时图像? cv::MOP_OPEN 开盘 没有 cv::MOP_CLOSE 闭幕 没有 cv::MOP_GRADIENT 形态梯度 总是 cv::MOP_TOPHAT 高顶礼帽 仅适用于就地(src = dst ) cv::MOP_BLACKHAT 黑帽 仅适用于就地(src = dst ) --------------------------------------------------- 开闭运算 在打开的情况下,我们首先侵蚀然后扩张 打开通常用于计算布尔图像中的区域。例如,如果我们在显微镜载玻片上对细胞图像进行阈值处理, 我们可能会在计算区域之前使用ning来分离彼此靠近的细胞。 在关闭的情况下,我们首先扩张然后侵蚀 Closing用于大多数更复杂的连通分量算法中以减少不需要的或噪音驱动的段。 对于连接组件, 通常首先执行侵蚀或关闭操作以消除纯粹由噪声产生的元素, 然后使用打开操作来连接附近的大区域(请注意,虽然使用开启或关闭的最终结果类似于使用侵蚀或扩张, 但这些新操作倾向于更准确地保留连接区域的区域) 当用于非布尔图像时,关闭的最显着效果是消除单个异常值 价值低于邻居, 而开放的影响是消除高于其邻居的孤立异常值 非升力图像使用时,关闭的最重要的前效应是去除低于其邻居价值的孤独外壳, 而打开的效应是去除孤独外壳,因为孤独外壳高于他们的邻居。 形态学梯度 梯度(SRC)=explate(SRC)–侵蚀(SRC) 从膨胀(稍微放大)图像中减去腐蚀(稍微缩小)图像的效果是在原始图像中留下对象边缘的表示。 我们看到操作符的值告诉我们图像亮度变化的速度; 这就是为什么“形态梯度”这个名称是合理的。 当我们想要分离明亮区域的周界时,通常使用形态梯度, 这样我们就可以把它们当作整个物体(或整个物体的一部分)来对待。 一个区域的完整周长往往会被发现,因为该区域的扩展版本减去了收缩版本,留下了完整的周长边缘。 这与计算梯度不同,梯度更不可能在物体的整个周长周围工作。 高顶礼帽和黑帽子 这些运算符用于隔离分别比其邻居更亮或更暗的补丁。 在尝试隔离仅显示相对于它们所附着的对象的亮度变化的对象部分时,可以使用这些。例如,这经常发生在生物体或细胞的显微镜图像上。这两个操作都是根据更原始的运算符定义的,如下所示: TopHat (src)= src - open (src)//隔离更亮 BlackHat (src)= close (src) - src //隔离调光器 如您所见,Top Hat操作员从A中减去A的打开形式。 回想一下,打开操作的效果是夸大小裂缝或局部下落。 因此,从A中减去open(A)应该揭示相对于内核大小比A的周围区域更轻的区域; 相反,黑帽操作员会发现比周围区域更暗的区域 A 制作自己的内核 到目前为止,我们所研究的形态学操作中,所考虑的核总是正方形的3×3。 如果您需要比这更一般的东西,OpenCV允许您创建自己的内核。 在形态学的情况下,内核通常被称为结构化元素, 因此允许您创建自己的形态学内核的例程被称为cv::getStructureingElement()。 实际上,您可以创建任何您喜欢的数组,并将其用作cv::explate()、cv::corrupt() 或cv::morphologyEx()等函数中的结构元素,但这通常比需要的工作更多。 通常你需要的是一个普通形状的非方形核。这是cv::getStructureingElement()的作用: 样cv::dilate() ,cv::erode() 或者cv::morphologyEx(), 但这是不是需要经常更多的工作。通常你需要的是一个非常常见形状的非方形内核。 这cv::getStructuringElement() 是为了什么: cv::Mat cv::getStructuringElement( int shape. //元素形状,例如cv :: MORPH_RECT cv::Size ksize, //结构元素的大小(奇数) cv::Point anchor = cv::point(-1,-1) //锚点的位置 ) cv :: getStructuringElement()元素形状 形状的价值 元件 描述 cv::MORPH_RECT Rectangular E i , j = 1, ∀ i , j cv::MORPH_ELLIPSE Elliptic Ellipse with axes ksize.width and ksize.height . cv::MORPH_CROSS Cross-shaped E i , j = 1, iff i == anchor .y or j == anchor .x 使用任意线性滤波器进行卷积 在我们迄今为止看到的函数中,卷积的基本机制发生在OpencvaAPI的底层。 我们花了一些时间了解卷积的基本知识,然后继续查看一长串实现不同类型有用卷积的函数。 实际上,在每一种情况下,都有一个内核是由我们选择的函数所隐含的, 我们只是传递了一些额外的信息,这些信息参数化了特定的过滤器类型。 然而,对于线性滤波器,我们可以只提供整个内核,让opencv为我们处理卷积。 从抽象的角度来看,这是非常简单的:我们只需要一个函数,用一个数组参数来描述内核, 我们就完成了。在实际水平上,有一个重要的微妙之处,它强烈影响性能。 这微妙之处在于有些内核是可分离的,而其他的则不是。 -1 0 +1 1 0 1 0 -2 0 +2 = 2 * -1 0 +1 1 1 1 -1 0 +1 1 0 1 0 A B C D 索贝尔核(A)是可分离的; 它可以表示为两个一维卷积(B和C); D是不可分离内核的示例 可分离的内核是可以被认为是两个一维内核的内核, 一个可分离的核可以看作是二维的核,我们首先用x-核卷积,然后用y-核卷积。 这种分解的好处是,核卷积的计算成本大约是图像面积乘以核面积。 这意味着用n×n核卷积A区域的图像需要与2成比例的时间,而用n×1核卷积图像一次, 然后用1×n核卷积图像需要与a+an=2an成比例的时间。 即使是小到3的N也有一个好处,而且好处随着N而增长。 使用cv::filter2d()应用常规筛选器 考虑到图像卷积所需的操作数,至少乍一看,20似乎是图像中的像素数乘以内核中的像素数, 这可能是一个很大的计算量,因此您不想对某些for循环和许多指针取消引用。 在这种情况下,最好让opencv为您完成工作,并利用内部优化。 所有这些操作的opencv方法都是使用cv::filter2d(): cv::filter2D( cv::InputArray src, //输入图像 cv::OutputArray dst, //结果图像 int ddepth, //输出深度 cv::InputArray kernel, //内核 cv::Point anchor = cv::Point(-1,-1), //瞄点 int borderType = cv::BORDER_DEFAULT //边界外推 ); 在这里,我们创建一个适当大小的数组,用线性滤波器的系数填充它, 然后将它与源图像和目标图像一起传递到cv::filter2D() 。 像往常一样,我们可以指定结果图像的深度,ddepth 过滤器的锚点anchor, 以及边界外推方法borderType 。如果定义了锚点,则内核可以是偶数大小; 否则,应该是奇怪的大小。如果要在应用线性过滤器后将总体偏移应用于结果,则可以使用该参数delta 。 使用cv :: sepFilter2D应用通用可分隔过滤器 在你的内核的情况下可分离的,你会得到的OpenCV的最佳性能, 通过以分离的形式表达它并将这些一维内核传递给OpenCV的内核,而不是图中所示的内核。 OpenCV函数cv::sepFilter2D() 类似cv::filter2D(), 只是它期望这两个一维内核而不是一个二维内核。 cv::sepFilter2D( cv::InputArray src, //输入图像 cv::OutputArray dst, //结果图像 int ddepth, //输出深度 cv::InputArray rowKernel, //1-by-N row内核 cv::InputArray columnKernel, //M-by-1列内核 cv::Point anchor = cv::Point(-1,-1), //瞄点的位置 double delta = 0, //赋值前的偏移量 int borderType = cv::BOEDER_DEFAULT //要使用的边界外推法 ) 除了 使用 和 参数替换参数之外,所有参数cv::sepFilter2D()都与其相同。 预期后两者是n 1 ×1和1× n 2 阵列(n 1 不一定等于n 2 )。 cv::filter2D()kernelrowKernelcolumnKernel 内核生成器 下面的函数可用于获得流行的内核: cv::getDerivasKernel()构造Sobel和Scharr内核, cv::getGaussiankernel()构造高斯内核。 cv::getDerivKernel() 派生过滤器的实际内核数组由cv::getDeriveKernel()生成。 void cv::getDerivKernel( cv :: OutputArray kx, cv :: OutputArray ky, int dx,// x中相应导数的顺序 int dy,// y中相应导数的顺序 int ksize,//内核大小 bool normalize = true,//如果为true,则除以框区域 int ktype = CV_32F //滤波器系数的类型 ); CV :: getGaussianKernel() 高斯滤波器的实际内核数组由cv::getGaussiankernel()生成。 cv :: Mat cv :: getGaussianKernel( int ksize,//内核大小 double sigma,//高斯半宽 int ktype = CV_32F //滤波器系数的类型 ); 与衍生内核一样,高斯内核是可分离的。 因此,cv::getGaussianKernel() 仅计算ksize ×1个系数阵列。 值ksize可以是任何奇数正数。该参数sigma设置近似高斯分布的标准偏差。 计算系数α,使得整个滤波器被归一化。sigma可以设置为-1 , 在这种情况下,将自动计算西格玛的值Size ksize
Talk is cheap. Show me the code