不同图像锐化算子提取的图像信息有哪些不同_opencv数字图像处理(3)- 图像平滑与锐化...

本节为 opencv数字图像处理(3): 灰度变换与空间滤波的第三小节, 图像平滑与锐化,主要包括:平滑线性滤波器(均值、盒装etc)、统计排序滤波器(中值etc)、拉普拉斯算子、梯度算子、边缘检测算子的对比及其C++实现代码。

3. 空间滤波

3.1 空间滤波基础

滤波的提法来自频域处理,频域处理中,滤波是指接受或拒绝一定的频率分量,例如低通滤波器(接受低频分量),最终完成图像的平滑。空间滤波直接作用于图像本身完成类似的平滑。(事实上, 线性空间滤波和频率域滤波之间存在一一对应的关系)

空间滤波器由一个邻域+对该邻域包围的图像像素执行的预定义操作组成。滤波产生的新像素的坐标记为邻域中心的坐标。如果在图像像素上执行的是线性操作,则该滤波器称为线性空间滤波器。

重点来看线性空间滤波,先明确两个概念,相关和卷积。相关是滤波器模板移过图像并计算每个位置乘积之和的处理。卷积的原理类似,只不过滤波器首先旋转180°。(为了使滤波器中每一个像素都可以访问图像中每一个像素,通常需要在图像四周进行填充,可行的方法有零填充,邻近元素填充等)

形式化地,一个mxn的滤波器w(x,y)与一幅图像f(x,y)做相关操作,可记为:

4718993b-6c13-eb11-8da9-e4434bdf6706.png

相似地,定义卷积操作:

4a18993b-6c13-eb11-8da9-e4434bdf6706.png

实际上很难在区分相关和卷积时去旋转滤波器,通常我们直接指定滤波器,一般也就不用过分区分相关和卷积。

3.2 平滑空间滤波器

平滑滤波器用于模糊处理和降低噪声,经常用于预处理任务中,通过线性滤波和非线性滤波处理,可以降低噪声。

3.2.1平滑线性滤波器

平滑线性空间滤波器的输出/响应是包含在滤波器模板邻域内的像素的简单平均值,这些滤波器也称为均值滤波器,是低通滤波器的一种。它使用滤波器模板确定的邻域内像素的平均灰度值代替图像中每个像素的值,这种处理的结果能够降低图像灰度的尖锐变化。由于典型的随机噪声是由灰度级急剧变化组成,因此常见的平滑处理应用就是降低噪声。

但是图像中不同区域的边缘也是由图像灰度尖锐变化带来的特性,所以均值滤波处理还是存在着不希望有的边缘模糊的负面效应,同时均值滤波处理可应用于灰度级数量不足而引起的伪轮廓效应的平滑处理。均值滤波器的主要应用是去除图像中的不相关细节。(“不相关”是指与滤波器模板尺寸相比,较小的像素区域)

所有系数都相等的空间均值滤波器也称为盒装滤波器;系数不同的模板则会产生加权平均,即像素之间的重要性是不一样的(一般中心的权重大,向四周扩展权重变小,这个系数可以由高斯函数计算,称为高斯模糊)。当图像细节与滤波器模板近似相同时,图像中的这些斜街收到影响会更大一点。

空间均值处理的一个重要应用是对感兴趣的物体得到一个粗略的描述而模糊一幅图像,这样较小物体的灰度就与背景混合在一起,较大物体变得像“斑点”,模板的大小由那些即将融入背景中的物体尺寸来决定。如下所示,从左到右分别是原图(528×485),滤波处理之后(15×15)和阈值处理之后的结果:

4d18993b-6c13-eb11-8da9-e4434bdf6706.png

统计排序滤波器(非线性)

这类滤波器的响应以滤波器包围的图像区域中所包含的像素的排序为基础,容纳后使用排序结果决定中心像素的值,最常见的即中值滤波器,它将像素邻域内灰度的中值代替该像素的值。中值滤波器使用非常普遍,这是因为对于一定类型的随机噪声,它提供了一种优秀的去噪能力,而且比相同尺寸的线性平滑滤波器的模糊成都明显要低。中值滤波器处理脉冲噪声非常有效(也成为椒盐噪声)

几种常见滤波的opencv实现如下所示,关于双边滤波,可以参考这里。另外和双边滤波类似的还有一个引导滤波

void allFilters()
{
 Mat srcImage = imread("test3.JPG");
 Mat boxfliter, mblur, gaussianblur, medianblur, bilateral;
 /* 方框滤波:该滤波器的响应是模板邻域像素值得平均值,也就是上面提到的模板系数全为1的盒装滤波器
 C++: void boxFilter(InputArray src, OutputArray dst, int ddepth, Size ksize, Point anchor=Point(-1,-1), bool normalize=true, int borderType=BORDER_DEFAULT )
 * src:输入图像
 * dst:输出图像(与输入图像等大)
 * ddepth:输出图像的深度,若为-1,则和输入图像相同
 * ksize:滤波器模板尺寸
 * anchor:锚点即被平滑的那个点,默认(-1,-1)代表锚点在滤波核的中心
 * normalize:默认为true,表示核是否被其区域归一化
 * borderType:用于推断突袭那个外部像素的某种边界模式,一般不同管
 */
 boxFilter(srcImage, boxfliter, -1, Size(7, 7));
 /*均值滤波:相当于调用normalize=true的方框滤波一样,只不过参数有些许的不同,模板系数均为1
 * src:输入图像,可以有任意的通道数,但是深度必须为CV_8U, CV_16U, CV_16S, CV_32F 或 CV_64F.
 */
 blur(srcImage, mblur, Size(7, 7));
 /* 高斯滤波:相当于模板系数由高斯函数计算生成的加权平均滤波,高斯滤波器是一种线性平滑滤波器,
  其滤波器的模板是对二维高斯函数离散得到。由于高斯模板的中心值最大,四周逐渐减小,其滤波后
  的结果相对于均值滤波器来说更好。高斯滤波器最重要的参数就是高斯分布的标准差,标准差和高斯
  滤波器的平滑能力有很大的能力,越大,高斯滤波器的频带就较宽,对图像的平滑程度就越好。通过
  调节参数,可以平衡对图像的噪声的抑制和对图像的模糊。
 * sigmaX,表示高斯核函数在X方向的的标准偏差
 * sigmaY,表示高斯核函数在Y方向的的标准偏差。若sigmaY为零,就将它设为sigmaX,
  如果sigmaX和sigmaY都是0,那么就由ksize.width和ksize.height计算出来。
 */
 GaussianBlur(srcImage, gaussianblur, Size(7, 7), 0, 0);
 /* 中值滤波
 * 只有三个参数,最后一个参数ksize必须是大于1的奇数,比如下面的应用中,我们将计算一个7x7邻域
 * 该函数对于多通道图像会逐通道处理。
 */
 medianBlur(srcImage, medianblur, 7);
 /* 双边滤波:作为一种非线性滤波器,可以保持边缘降噪平滑,其采用加权平均的方法,系数基于高斯分布产生,
  但是重要的是,双边滤波的权重不仅考虑了像素的欧式距离,还考虑了像素范围域内的辐射差异
  (例如:相似程度,颜色强度,深度距离等),是一种边缘保护滤波方法
 * 双边滤波的核函数是空间域核与像素范围域核的综合结果:在图像的平坦区域,像素值变化很小,对应的像素范
  围域权重接近于1,此时空间域权重起主要作用,相当于进行高斯模糊;在图像的边缘区域,像素值变化很大,
  像素范围域权重变大,从而保持了边缘的信息。
 void bilateralFilter( InputArray src, OutputArray dst, int d,
           double sigmaColor, double sigmaSpace,
           int borderType = BORDER_DEFAULT );
 * src:输入图像
 * dst: 输出图像
 * d:滤波窗口的直径(函数注释中使用的是Diameter,那么很可能函数中选取的窗口是圆形窗口)
 * sigmaColor:像素值域方差
 * sigmaSpace:空间域方差
 */
 bilateralFilter(srcImage, bilateral, 25, 25 * 2, 25 / 2);
 imshow("ori", srcImage);
 imshow("box", boxfliter);
 imshow("blur", mblur);
 imshow("gaussian", gaussianblur);
 imshow("median", medianblur);
 imshow("bilateral", bilateral);
 waitKey(0);
}

3.3 锐化空间滤波器

锐化处理的主要目的是突出灰度的过渡部分,锐化处理可以有空间微分来实现。基本上,微分算子的响应强度与图像在用算子操作的这一点的突变程度成正比,这样,图像微分增强边缘和其他突变(比如噪声~),而削弱灰度变化缓慢的区域。

数字函数的一阶微分可以用不同术语定义,但是对于一阶微分的任何定义都必须保证:

  • 恒定灰度区域的微分值为0
  • 在灰度台阶或斜坡处微分值非0
  • 沿着斜坡的微分值非0

类似地,二阶微分的定义也必须保证:

  • 恒定区域的微分值为0
  • 在灰度台阶或斜坡的起点处微分值非0
  • 沿着斜坡的微分值非0

对于一维函数,其一阶微分的基本定义是差值:

(使用偏导数符号是为了与二阶图像函数f(x,y)的微分保持一致)。

对于二维函数,将沿着两个空间轴处理偏微分,将二阶微分定义为差分

如下图所示的图像的一段扫描线,小方块的数值是扫描线中的灰度值,其中包括了一个灰度斜坡,三个恒定灰度段和一个灰度台阶:

5018993b-6c13-eb11-8da9-e4434bdf6706.png

下图计算了扫描线的一阶和二阶微分:

5318993b-6c13-eb11-8da9-e4434bdf6706.png

可以验证图中的值是符合微分定义的。 数字图像中的边缘在灰度上通常类似于斜坡过渡,这样就导致图像的一阶微分产生较粗的边缘因为沿着斜坡的微分非0。另一方面,二阶微分产生由0分开的一个像素宽的双边缘。这样可知,二阶微分在增强细节方面要比一阶微分好得多,这是一个适合锐化图像的理想特性。

3.3.1 拉普拉斯算子-使用二阶微分进行图像锐化

二维函数二阶微分用于图像锐化,基本上是定义一个二阶微分的离散公式,然后基于该公式构造一个滤波器模板。其中,各向同性滤波器的响应与滤波器作用的图像的突变方向无关,是旋转不变的,即原图像旋转和进行滤波处理给出的结果与先对图像滤波再旋转的结果相同。

各向同性微分算子中最简单的是拉普拉斯算子,一个二维图像函数f(x,y)的拉普拉斯算子定义为:

5518993b-6c13-eb11-8da9-e4434bdf6706.png

因为任意阶微分都是线性操作,所以拉普拉斯变换也是一个线性算子。为了以离散形式描述上公式,在x方向上有:

5918993b-6c13-eb11-8da9-e4434bdf6706.png

类似地,y方向上有:

5d18993b-6c13-eb11-8da9-e4434bdf6706.png

这样离散拉普拉斯算子定义为:

6118993b-6c13-eb11-8da9-e4434bdf6706.png

由于拉普拉斯是一种微分算子,其应用强调的是图像中灰度的突变而不是灰度级缓慢变化的区域,这将产生吧浅灰色边线和突变点叠加到暗色背景中的图像。将原图像和拉普拉斯图像叠加在一起,可以复原背景特性并保持拉普拉斯锐化处理的效果。需要注意的是,如果使用的拉普拉斯滤波器中心系数为负,则应该将原图像减去拉普拉斯变换图像,因此使用拉普拉斯增强图像的方式可以形式化为:

6618993b-6c13-eb11-8da9-e4434bdf6706.png

其中,如果使用下图左边两个拉普拉斯滤波器,则c=-1,使用另外两个,常数c=1。

6c18993b-6c13-eb11-8da9-e4434bdf6706.png

下图2显示了使用上图2所示的拉普拉斯模板对下图1所示的图像进行滤波后的结果,由于拉普拉斯图像中既有正值又有负值,并且所有复制在显示时都修剪为0,所以图像的大部分是黑色的。一个典型的标定拉普拉斯图像的方法是对它的最小值加一个新的代替0的最小值,然后讲解过标定到整个灰度范围[0,L-1]内,下图3所示的图像就是使用该方法标定过的图像,该图像的支配特性是边缘和灰度的不连续处,图2的黑色背景由于标定现在已变成灰色,这一呈现浅灰色的外观的图像是被适当标定的典型拉普拉斯图像。下图4显示了在c=-1时得到的结果,该图细节更清晰,将原图像加到拉普拉斯处理结果中,可以使图像中的各灰度值得到复原,而且通过拉普拉斯增强了图像中灰度突变处的对比度。下图5显示了使用上图2中的滤波器重复上述过程的结果。

6f18993b-6c13-eb11-8da9-e4434bdf6706.png

使用opencv自带的拉普拉斯算子C++代码:

void Laplacian_Opencv()
{
 Mat srcImage = imread("test3.JPG");
 Mat dstImage;
 滤波消除噪声
 //GaussianBlur(srcImage, srcImage, Size(3, 3), 0, 0, BORDER_DEFAULT);
 // 该函数默认ksize=1,此时的滤波器模板为{0}{1}{0}{1}{-4}{1}{0}{1}{0}
 Laplacian(srcImage, dstImage, srcImage.depth());
 imshow("ori", srcImage);
 imshow("La", dstImage);
 dstImage = srcImage - dstImage;
 imshow("dst", dstImage);
 waitKey(0);
}

自己实现的拉普拉斯算子C++代码:

void Laplacian_Mine()
{
 Mat srcImage = imread("test6.JPG", 0);
 GaussianBlur(srcImage, srcImage, Size(3, 3), 0, 0, BORDER_DEFAULT);
 imshow("ori", srcImage);
 int mask[9] = { -1,-1,-1,-1,8,-1,-1,-1,-1 };
 //int mask[9] = { 0,1,0,1,-4,1,0,1,0 };

 int nr = srcImage.rows;
 int nc = srcImage.cols;
 int n = nr * nc;
 int arr[9] = { 0 };

 int* table_lap = new int[n];
 int* table_orig = new int[n];
 int l;
 for (int i = 0; i < n; i++)
 {
  table_lap[i] = 0;
  table_orig[i] = 0;
 }
 for (int i = 1; i < nr - 1; i++)
 {
  const uchar* previous = srcImage.ptr<uchar>(i - 1);
  const uchar* current = srcImage.ptr<uchar>(i);
  const uchar* next = srcImage.ptr<uchar>(i + 1);
  for (int j = 1; j < nc - 1; j++)
  {
   for (int k = 0; k < 3; k++)
   {
    arr[k] = previous[j + k - 1];
    arr[k + 3] = current[j + k - 1];
    arr[k + 6] = next[j + k - 1];
   }
   l = nc * i + j;        //calculate the location in the table of current pixel
   for (int mf = 0; mf < 9; mf++)
   {
    table_lap[l] = table_lap[l] + mask[mf] * arr[mf];
   }
   table_orig[l] = arr[4];
  }
 }
 //标定,灰度拉伸至(0,255)
 uchar* La_scaled = new uchar[n];
 int min = table_lap[0];
 int max = table_lap[0];
 for (int i = 0; i < n; i++)
 {
  if (min > table_lap[i])
   min = table_lap[i];
  if (max < table_lap[i])
   max = table_lap[i];
 }
 for (int i = 0; i < n; i++) 
 {
  La_scaled[i]= (uchar)(255 * (table_lap[i] - min) / (max - min));
 }
 //储存标定后的拉普拉斯结果
 Mat LaRes;
 LaRes.create(srcImage.size(), srcImage.type());
 for (int i = 0; i < nr; i++) 
 {
  uchar* p = LaRes.ptr<uchar>(i);
  for (int j = 0; j < nc; j++)
  {
   l = nc * i + j;
   //p[j] = La_scaled[l];
   p[j] = table_orig[l] + table_lap[l];
   if (p[j] > 255)p[j] = 255;
   if (p[j] < 0)p[j] = 0;
  }
 }
 imshow("LaRes", LaRes);
 waitKey();
}

3.3.2 非锐化掩蔽和高提升滤波

非锐化掩蔽过程如下所示:

  • 模糊原图像
  • 从原图像中减去模糊图像(产生的茶之称为模板)
  • 将模板加到原图像上

表示模糊图像,非锐化掩蔽可形式化如下,首先得到模板:
,然后在原图像上加上该模板的一个权重部分:
,当k=1时非锐化掩蔽,当k>1时称为高提升滤波,若k<1,则不强调非锐化模板的贡献。

非锐化掩蔽的原理可由下图说明:

7118993b-6c13-eb11-8da9-e4434bdf6706.png

3.3.3 梯度-使用一阶微分对(非线性)图像锐化 对于函数f(x,y),f在坐标(x,y)处的梯度定义为:

7518993b-6c13-eb11-8da9-e4434bdf6706.png

它给出了在位置(x,y)处f的最大变化率的方向,向量

的幅度值表示为M(x,y),即:

7818993b-6c13-eb11-8da9-e4434bdf6706.png

这里的M(x,y)是与原图像大小相同的图像,实践中,该图像称为梯度图像。

因为梯度向量的分量是微分,所以它们是线性算子,然而该向量的幅度不是线性算子,因为求幅度是做平方和平方根操作。在某些实现中,用绝对值来近似平方和平方根操作更便于计算:

7b18993b-6c13-eb11-8da9-e4434bdf6706.png

该表达式保留了灰度的相对变化但是各向同性特性丢失。对上述公式定义一个离散近似,由此形成合适的滤波模板:考虑一个3x3区域内的图像,令中心点

表示任意位置(x,y)处的f(x,y),
表示为f(x-1,y-1),这样对一阶微分的最简近似是
。但是这里我们取其他两个定义使用交叉差分:

7e18993b-6c13-eb11-8da9-e4434bdf6706.png

上式中所需的偏微分项可以用下图中的两个线性滤波器模板来实现,这些模板称为罗伯特交叉梯度算子:

8018993b-6c13-eb11-8da9-e4434bdf6706.png

这样计算梯度图像为:

8318993b-6c13-eb11-8da9-e4434bdf6706.png

如果用绝对值的方式近似表示,则:

8618993b-6c13-eb11-8da9-e4434bdf6706.png

通常我们更希望得到奇数尺寸的模板,考虑最小模板3x3模板,使用

为中心的一个3x3邻域对
的近似如下所示:

8918993b-6c13-eb11-8da9-e4434bdf6706.png

这两个公式可以使用下图所示的两个模板实现。使用下左图中的模板实现图像区域第三行和第一行的差近似x方向的偏微分,使用下右图中的模板的第三列和第一列的差近似y方向的微分。下图所示的模板称为Soble算子:

8b18993b-6c13-eb11-8da9-e4434bdf6706.png

计算完成后代入公式计算梯度幅值如下:

8e18993b-6c13-eb11-8da9-e4434bdf6706.png

注意上面的Sobel算子,中心系数使用权重2的思想是通过突出中心点的作用达到平滑的目的。而且上面提到的两个模板系数总和为0,表明灰度恒定区域的响应为0。

opencv自带的Sobel算子使用代码如下:

void Sobel_Opencv()
{
 Mat srcImage = imread("test6.JPG");
 imshow("ori", srcImage);
 Mat xdstImage, ydstImage, dstImage;
 /* Sobel (
 * InputArray src,//输入图
 * OutputArray dst,//输出图
 * int ddepth,//输出图像的深度
 * int dx,    // x 方向上的差分阶数
 * int dy,     // y方向上的差分阶数
 * int ksize=3, // 有默认值3,表示Sobel核的大小;必须取1,3,5或7
 * double scale=1,
 * double delta=0,
 * int borderType=BORDER_DEFAULT );
 */
 Sobel(srcImage, xdstImage, -1, 1, 0);
 imshow("xdst", xdstImage);
 Sobel(srcImage, ydstImage, -1, 0, 1);
 imshow("ydst", ydstImage);
 addWeighted(xdstImage, 0.5, ydstImage, 0.5, 1, dstImage);
 imshow("dst", dstImage);
 waitKey(0);
}

也可以自己实现,参考这篇博文,C++代码如下:

void Sobel_Mine()
{
 Mat m_img = imread("test6.JPG");
 Mat src(m_img.rows, m_img.cols, CV_8UC1, Scalar(0));
 cvtColor(m_img, src, CV_RGB2GRAY);

 Mat dstImage(src.rows, src.cols, CV_8UC1, Scalar(0));
 for (int i = 1; i < src.rows - 1; i++)
 {
  for (int j = 1; j < src.cols - 1; j++)
  {
   dstImage.data[i * dstImage.step + j] = sqrt((src.data[(i - 1) * src.step + j + 1]
    + 2 * src.data[i * src.step + j + 1]
    + src.data[(i + 1) * src.step + j + 1]
    - src.data[(i - 1) * src.step + j - 1] - 2 * src.data[i * src.step + j - 1]
    - src.data[(i + 1) * src.step + j - 1]) * (src.data[(i - 1) * src.step + j + 1]
     + 2 * src.data[i * src.step + j + 1] + src.data[(i + 1) * src.step + j + 1]
     - src.data[(i - 1) * src.step + j - 1] - 2 * src.data[i * src.step + j - 1]
     - src.data[(i + 1) * src.step + j - 1]) + (src.data[(i - 1) * src.step + j - 1] + 2 * src.data[(i - 1) * src.step + j]
      + src.data[(i - 1) * src.step + j + 1] - src.data[(i + 1) * src.step + j - 1]
      - 2 * src.data[(i + 1) * src.step + j]
      - src.data[(i + 1) * src.step + j + 1]) * (src.data[(i - 1) * src.step + j - 1] + 2 * src.data[(i - 1) * src.step + j]
       + src.data[(i - 1) * src.step + j + 1] - src.data[(i + 1) * src.step + j - 1]
       - 2 * src.data[(i + 1) * src.step + j]
       - src.data[(i + 1) * src.step + j + 1]));

  }

 }
 Mat grad_y(src.rows, src.cols, CV_8UC1, Scalar(0));
 {
  for (int i = 1; i < src.rows - 1; i++)
  {
   for (int j = 1; j < src.cols - 1; j++)
   {
    grad_y.data[i * grad_y.step + j] = abs((src.data[(i - 1) * src.step + j + 1]
     + 2 * src.data[i * src.step + j + 1]
     + src.data[(i + 1) * src.step + j + 1]
     - src.data[(i - 1) * src.step + j - 1] - 2 * src.data[i * src.step + j - 1]
     - src.data[(i + 1) * src.step + j - 1]));
   }
  }
 }
 Mat grad_x(src.rows, src.cols, CV_8UC1, Scalar(0));
 {
  for (int i = 1; i < src.rows - 1; i++)
  {
   for (int j = 1; j < src.cols - 1; j++)
   {
    grad_x.data[i * grad_x.step + j] = sqrt((src.data[(i - 1) * src.step + j - 1] + 2 * src.data[(i - 1) * src.step + j]
     + src.data[(i - 1) * src.step + j + 1] - src.data[(i + 1) * src.step + j - 1]
     - 2 * src.data[(i + 1) * src.step + j]
     - src.data[(i + 1) * src.step + j + 1]) * (src.data[(i - 1) * src.step + j - 1] + 2 * src.data[(i - 1) * src.step + j]
      + src.data[(i - 1) * src.step + j + 1] - src.data[(i + 1) * src.step + j - 1]
      - 2 * src.data[(i + 1) * src.step + j]
      - src.data[(i + 1) * src.step + j + 1]));
   }
  }
 }
 imshow("原图", src);
 imshow("gradient", dstImage);
 imshow("Vertical gradient", grad_y);
 imshow("Horizontal gradient", grad_x);
 imshow("res", src + dstImage);
 imshow("resx", src + grad_x);
 imshow("resy", src + grad_y);
 waitKey(0);
}

3.4 几种边缘检测算子的比较

Laplacian算子

是二阶微分算子。其具有各向同性,即与坐标轴方向无关,坐标轴旋转后梯度结果不变。但是,其对噪声比较敏感,所以,图像一般先经过平滑处理,因为平滑处理也是用模板进行的,所以,通常的分割算法都是把Laplacian算子和平滑算子结合起来生成一个新的模板。

Laplacian算子一般不以其原始形式用于边缘检测,因为其作为一个二阶导数,Laplacian算子对噪声具有无法接受的敏感性;同时其幅值产生算边缘,这是复杂的分割不希望有的结果;最后Laplacian算子不能检测边缘的方向;所以Laplacian在分割中所起的作用包括:

  • (1)利用它的零交叉性质进行边缘定位;
  • (2)确定一个像素是在一条边缘暗的一面还是亮的一面;

如果在图像中一个较暗的区域中出现了一个亮点,那么用拉普拉斯运算就会使这个亮点变得更亮。因为图像中的边缘就是那些灰度发生跳变的区域,所以拉普拉斯锐化模板在边缘检测中很有用。一般增强技术对于陡峭的边缘和缓慢变化的边缘很难确定其边缘线的位置。但此算子却可用二次微分正峰和负峰之间的过零点来确定,对孤立点或端点更为敏感,因此特别适用于以突出图像中的孤立点、孤立线或线端点为目的的场合。同梯度算子一样,拉普拉斯算子也会增强图像中的噪声,有时用拉普拉斯算子进行边缘检测时,可将图像先进行平滑处理。

一般使用的是高斯型拉普拉斯算子(Laplacian of a Gaussian,LoG),由于二阶导数是线性运算,利用LoG卷积一幅图像与首先使用高斯型平滑函数卷积改图像,然后计算所得结果的拉普拉斯是一样的。所以在LoG公式中使用高斯函数的目的就是对图像进行平滑处理,使用Laplacian算子的目的是提供一幅用零交叉确定边缘位置的图像;图像的平滑处理减少了噪声的影响并且它的主要作用还是抵消由Laplacian算子的二阶导数引起的逐渐增加的噪声影响。

Roberts算子

是一种梯度算子,边缘定位准,但是对噪声敏感。适用于边缘明显且噪声较少的图像分割。Roberts边缘检测算子是一种利用局部差分算子寻找边缘的算子,Robert算子图像处理后结果边缘不是很平滑。由于Robert算子通常会在图像边缘附近的区域内产生较宽的响应,故采用上述算子检测的边缘图像常需做细化处理,边缘定位的精度不是很高。

Sobel算子

梯度算子,是典型的基于一阶导数的边缘检测算子,由于该算子中引入了类似局部平均的运算,因此对噪声具有平滑作用,能很好的消除噪声的影响。此外,Sobel算子对于像素的位置的影响做了加权,离中心位置越远的像素贡献度越低。

Prewitt算子

梯度算子,利用像素点上下、左右邻点的灰度差,在边缘处达到极值检测边缘,去掉部分伪边缘,对噪声具有平滑作用 。其原理是在图像空间利用两个方向模板与图像进行邻域卷积来完成的,这两个方向模板一个检测水平边缘,一个检测垂直边缘。

Prewitt算子对噪声有抑制作用,抑制噪声的原理是通过像素平均,但是像素平均相当于对图像的低通滤波,所以Prewitt算子对边缘的定位不如Roberts算子。

该算子与Sobel算子类似,只是权值有所变化,但两者实现起来功能还是有差距的,据经验得知Sobel要比Prewitt更能准确检测图像边缘。

Sobel的模板系数:水平方向[[-1, -2, -1], [0, 0, 0], [1, 2, 1]],垂直方向[[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]; Prewitt算在的模板系数:水平方向[[-1, -1, -1], [0, 0, 0], [1, 1, 1]],垂直方向[[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]]

Sobel算子还有另外一种形式——Isotropic Sobel算子,权值反比零点与中心店的距离,当沿不同方向检测边缘时梯度幅度一致,就是通常所说的各向同性Sobel(Isotropic Sobel)算子。模板也有两个,一个是检测水平边沿的 ,另一个是检测垂直平边沿的 。各向同性Sobel算子和普通Sobel算子相比,它的位置加权系数更为准确,在检测不同方向的边沿时梯度的幅度一致。

Canny算子

Canny边缘检测是一种比较新的边缘检测算子,具有很好地边缘检测性能,该算子功能比前面几种都要好,但是它实现起来较为麻烦,Canny算子是一个具有滤波,增强,检测的多阶段的优化算子,在进行处理前,Canny算子先利用高斯平滑滤波器来平滑图像以除去噪声,Canny分割算法采用一阶偏导的有限差分来计算梯度幅值和方向,在处理过程中,Canny算子还将经过一个非极大值抑制的过程,最后Canny算子还采用两个阈值来连接边缘(高低阈值输出二值图像)。

Canny算子求边缘点具体算法步骤如下:

  • 用高斯滤波器平滑图像

高斯卷积核的大小将影响Canny检测器的性能。尺寸越大,去噪能力越强,因此噪声越少,但图片越模糊,canny检测算法抗噪声能力越强,但模糊的副作用也会导致定位精度不高,一般情况下推荐尺寸5x5,3x3尺寸.

  • 用一阶偏导有限差分计算梯度幅值和方向

梯度和方向角的计算,边缘的最重要的特征是灰度值剧烈变化,如果把灰度值看成二元函数值,那么灰度值的变化可以用二元函数的”导数“(或者称为梯度)来描述。由于图像是离散数据,导数可以用差分值来表示,差分在实际工程中就是灰度差,说人话就是两个像素的差值。一个像素点有8邻域,那么分上下左右斜对角,因此Canny算法使用四个算子来检测图像中的水平、垂直和对角边缘。算子是以图像卷积的形式来计算梯度,比如Roberts,Prewitt,Sobel等

  • 对梯度幅值进行非极大值抑制

图像梯度幅值矩阵中的元素值越大,说明图像中该点的梯度值越大,但这不能说明该点就是边缘(这仅仅是属于图像增强的过程)。在Canny算法中,非极大值抑制是进行边缘检测的重要步骤,通俗意义上是指寻找像素点局部最大值,将非极大值点所对应的灰度值置为0,这样可以剔除掉一大部分非边缘的点。

9018993b-6c13-eb11-8da9-e4434bdf6706.png

根据上图可知,要进行非极大值抑制,就首先要确定像素点C的灰度值在其8值邻域内是否为最大。图中蓝色的线条方向为C点的梯度方向,这样就可以确定其局部的最大值肯定分布在这条线上,也即出了C点外,梯度方向的交点dTmp1和dTmp2这两个点的值也可能会是局部最大值。因此,判断C点灰度与这两个点灰度大小即可判断C点是否为其邻域内的局部最大灰度点。如果经过判断,C点灰度值小于这两个点中的任一个,那就说明C点不是局部极大值,那么则可以排除C点为边缘。这就是非极大值抑制的工作原理。

  • 用双阈值算法检测和连接边缘.

我们假设两类边缘:经过非极大值抑制之后的边缘点中,梯度值超过T1的称为强边缘,梯度值小于T1大于T2的称为弱边缘,梯度小于T2的不是边缘。可以肯定的是,强边缘必然是边缘点,因此必须将T1设置的足够高,以要求像素点的梯度值足够大(变化足够剧烈),而弱边缘可能是边缘,也可能是噪声,如何判断呢?当弱边缘的周围8邻域有强边缘点存在时,就将该弱边缘点变成强边缘点,以此来实现对强边缘的补充。

也就是说①:如果该像素的梯度值小于TlTl,则该像素为非边缘像素;②:如果该像素的梯度值大于ThTh,则该像素为边缘像素;③:如果该像素的梯度值介于TlTl与ThTh之间,需要进一步检测该像素的3×33×3邻域内的8个点,如果这8个点内有一个或以上的点梯度超过了ThTh,则该像素为边缘像素,否则不是边缘像素。

C++代码实现如下(可以看一下这篇博文):

#include "core/core.hpp"  
#include "highgui/highgui.hpp"  
#include "imgproc/imgproc.hpp"  
#include "iostream"
#include "math.h"
 
using namespace std; 
using namespace cv;  
 
//******************灰度转换函数*************************
//第一个参数image输入的彩色RGB图像;
//第二个参数imageGray是转换后输出的灰度图像;
//*************************************************************
void ConvertRGB2GRAY(const Mat &image,Mat &imageGray);
 
 
//******************高斯卷积核生成函数*************************
//第一个参数gaus是一个指向含有N个double类型数组的指针;
//第二个参数size是高斯卷积核的尺寸大小;
//第三个参数sigma是卷积核的标准差
//*************************************************************
void GetGaussianKernel(double **gaus, const int size,const double sigma);
 
//******************高斯滤波*************************
//第一个参数imageSource是待滤波原始图像;
//第二个参数imageGaussian是滤波后输出图像;
//第三个参数gaus是一个指向含有N个double类型数组的指针;
//第四个参数size是滤波核的尺寸
//*************************************************************
void GaussianFilter(const Mat imageSource,Mat &imageGaussian,double **gaus,int size);
 
//******************Sobel算子计算梯度和方向********************
//第一个参数imageSourc原始灰度图像;
//第二个参数imageSobelX是X方向梯度图像;
//第三个参数imageSobelY是Y方向梯度图像;
//第四个参数pointDrection是梯度方向数组指针
//*************************************************************
void SobelGradDirction(const Mat imageSource,Mat &imageSobelX,Mat &imageSobelY,double *&pointDrection);
 
//******************计算Sobel的X和Y方向梯度幅值*************************
//第一个参数imageGradX是X方向梯度图像;
//第二个参数imageGradY是Y方向梯度图像;
//第三个参数SobelAmpXY是输出的X、Y方向梯度图像幅值
//*************************************************************
void SobelAmplitude(const Mat imageGradX,const Mat imageGradY,Mat &SobelAmpXY);
 
//******************局部极大值抑制*************************
//第一个参数imageInput输入的Sobel梯度图像;
//第二个参数imageOutPut是输出的局部极大值抑制图像;
//第三个参数pointDrection是图像上每个点的梯度方向数组指针
//*************************************************************
void LocalMaxValue(const Mat imageInput,Mat &imageOutput,double *pointDrection);
 
//******************双阈值处理*************************
//第一个参数imageInput输入和输出的的Sobel梯度幅值图像;
//第二个参数lowThreshold是低阈值
//第三个参数highThreshold是高阈值
//******************************************************
void DoubleThreshold(Mat &imageIput,double lowThreshold,double highThreshold);
 
//******************双阈值中间像素连接处理*********************
//第一个参数imageInput输入和输出的的Sobel梯度幅值图像;
//第二个参数lowThreshold是低阈值
//第三个参数highThreshold是高阈值
//*************************************************************
void DoubleThresholdLink(Mat &imageInput,double lowThreshold,double highThreshold);
 
Mat imageSource;
Mat imageGray;
Mat imageGaussian;
 
int main(int argc,char *argv[])  
{
 imageSource=imread(argv[1]);  //读入RGB图像
 imshow("RGB Image",imageSource);
 ConvertRGB2GRAY(imageSource,imageGray); //RGB转换为灰度图
 imshow("Gray Image",imageGray);
 int size=5; //定义卷积核大小
 double **gaus=new double *[size];  //卷积核数组
 for(int i=0;i<size;i++)
 {
  gaus[i]=new double[size];  //动态生成矩阵
 } 
 GetGaussianKernel(gaus,5,1); //生成5*5 大小高斯卷积核,Sigma=1;
 imageGaussian=Mat::zeros(imageGray.size(),CV_8UC1);
 GaussianFilter(imageGray,imageGaussian,gaus,5);  //高斯滤波
 imshow("Gaussian Image",imageGaussian);
 Mat imageSobelY;
 Mat imageSobelX;
 double *pointDirection=new double[(imageSobelX.cols-1)*(imageSobelX.rows-1)];  //定义梯度方向角数组
 SobelGradDirction(imageGaussian,imageSobelX,imageSobelY,pointDirection);  //计算X、Y方向梯度和方向角
 imshow("Sobel Y",imageSobelY);
 imshow("Sobel X",imageSobelX);
 Mat SobelGradAmpl;
 SobelAmplitude(imageSobelX,imageSobelY,SobelGradAmpl);   //计算X、Y方向梯度融合幅值
 imshow("Soble XYRange",SobelGradAmpl);
 Mat imageLocalMax;
 LocalMaxValue(SobelGradAmpl,imageLocalMax,pointDirection);  //局部非极大值抑制
 imshow("Non-Maximum Image",imageLocalMax);
 Mat cannyImage;
 cannyImage=Mat::zeros(imageLocalMax.size(),CV_8UC1);
 DoubleThreshold(imageLocalMax,90,160);        //双阈值处理
 imshow("Double Threshold Image",imageLocalMax);
 DoubleThresholdLink(imageLocalMax,90,160);   //双阈值中间阈值滤除及连接
 imshow("Canny Image",imageLocalMax);
 waitKey();
 system("pause");
 return 0;
}
 
//******************高斯卷积核生成函数*************************
//第一个参数gaus是一个指向含有N个double类型数组的指针;
//第二个参数size是高斯卷积核的尺寸大小;
//第三个参数sigma是卷积核的标准差
//*************************************************************
void GetGaussianKernel(double **gaus, const int size,const double sigma)
{
 const double PI=4.0*atan(1.0); //圆周率π赋值
 int center=size/2;
 double sum=0;
 for(int i=0;i<size;i++)
 {
  for(int j=0;j<size;j++)
  {
   gaus[i][j]=(1/(2*PI*sigma*sigma))*exp(-((i-center)*(i-center)+(j-center)*(j-center))/(2*sigma*sigma));
   sum+=gaus[i][j];
  }
 }
 for(int i=0;i<size;i++)
 {
  for(int j=0;j<size;j++)
  {
   gaus[i][j]/=sum;
   cout<<gaus[i][j]<<"  ";
  }
  cout<<endl<<endl;
 }
 return ;
}
 
//******************灰度转换函数*************************
//第一个参数image输入的彩色RGB图像;
//第二个参数imageGray是转换后输出的灰度图像;
//*************************************************************
void ConvertRGB2GRAY(const Mat &image,Mat &imageGray)
{
 if(!image.data||image.channels()!=3)
 {
  return ;
 }
 imageGray=Mat::zeros(image.size(),CV_8UC1);
 uchar *pointImage=image.data;
 uchar *pointImageGray=imageGray.data;
 int stepImage=image.step;
 int stepImageGray=imageGray.step;
 for(int i=0;i<imageGray.rows;i++)
 {
  for(int j=0;j<imageGray.cols;j++)
  {
   pointImageGray[i*stepImageGray+j]=0.114*pointImage[i*stepImage+3*j]+0.587*pointImage[i*stepImage+3*j+1]+0.299*pointImage[i*stepImage+3*j+2];
  }
 }
}
 
//******************高斯滤波*************************
//第一个参数imageSource是待滤波原始图像;
//第二个参数imageGaussian是滤波后输出图像;
//第三个参数gaus是一个指向含有N个double类型数组的指针;
//第四个参数size是滤波核的尺寸
//*************************************************************
void GaussianFilter(const Mat imageSource,Mat &imageGaussian,double **gaus,int size)
{
 imageGaussian=Mat::zeros(imageSource.size(),CV_8UC1);
 if(!imageSource.data||imageSource.channels()!=1)
 {
  return ;
 }
 double gausArray[100]; 
 for(int i=0;i<size*size;i++)
 {
  gausArray[i]=0;  //赋初值,空间分配
 }
 int array=0;
 for(int i=0;i<size;i++)
 {
  for(int j=0;j<size;j++)
 
  {
   gausArray[array]=gaus[i][j];//二维数组到一维 方便计算
   array++;
  }
 }
 //滤波
 for(int i=0;i<imageSource.rows;i++)
 {
  for(int j=0;j<imageSource.cols;j++)
  {
   int k=0;
   for(int l=-size/2;l<=size/2;l++)
   {
    for(int g=-size/2;g<=size/2;g++)
    {
     //以下处理针对滤波后图像边界处理,为超出边界的值赋值为边界值
     int row=i+l;
     int col=j+g;
     row=row<0?0:row;
     row=row>=imageSource.rows?imageSource.rows-1:row;
     col=col<0?0:col;
     col=col>=imageSource.cols?imageSource.cols-1:col;
     //卷积和
     imageGaussian.at<uchar>(i,j)+=gausArray[k]*imageSource.at<uchar>(row,col);
     k++;
    }
   }
  }
 }
}
//******************Sobel算子计算X、Y方向梯度和梯度方向角********************
//第一个参数imageSourc原始灰度图像;
//第二个参数imageSobelX是X方向梯度图像;
//第三个参数imageSobelY是Y方向梯度图像;
//第四个参数pointDrection是梯度方向角数组指针
//*************************************************************
void SobelGradDirction(const Mat imageSource,Mat &imageSobelX,Mat &imageSobelY,double *&pointDrection)
{
 pointDrection=new double[(imageSource.rows-1)*(imageSource.cols-1)];
 for(int i=0;i<(imageSource.rows-1)*(imageSource.cols-1);i++)
 {
  pointDrection[i]=0;
 }
 imageSobelX=Mat::zeros(imageSource.size(),CV_32SC1);
 imageSobelY=Mat::zeros(imageSource.size(),CV_32SC1);
 uchar *P=imageSource.data;  
 uchar *PX=imageSobelX.data;  
 uchar *PY=imageSobelY.data;  
 
 int step=imageSource.step;  
 int stepXY=imageSobelX.step; 
 int k=0;
 int m=0;
 int n=0;
 for(int i=1;i<(imageSource.rows-1);i++)  
 {  
  for(int j=1;j<(imageSource.cols-1);j++)  
  {  
   //通过指针遍历图像上每一个像素 
   double gradY=P[(i-1)*step+j+1]+P[i*step+j+1]*2+P[(i+1)*step+j+1]-P[(i-1)*step+j-1]-P[i*step+j-1]*2-P[(i+1)*step+j-1];
   PY[i*stepXY+j*(stepXY/step)]=abs(gradY);
   double gradX=P[(i+1)*step+j-1]+P[(i+1)*step+j]*2+P[(i+1)*step+j+1]-P[(i-1)*step+j-1]-P[(i-1)*step+j]*2-P[(i-1)*step+j+1];
            PX[i*stepXY+j*(stepXY/step)]=abs(gradX);
   if(gradX==0)
   {
    gradX=0.00000000000000001;  //防止除数为0异常
   }
   pointDrection[k]=atan(gradY/gradX)*57.3;//弧度转换为度
   pointDrection[k]+=90;
   k++;
  }  
 } 
 convertScaleAbs(imageSobelX,imageSobelX);
 convertScaleAbs(imageSobelY,imageSobelY);
}
//******************计算Sobel的X和Y方向梯度幅值*************************
//第一个参数imageGradX是X方向梯度图像;
//第二个参数imageGradY是Y方向梯度图像;
//第三个参数SobelAmpXY是输出的X、Y方向梯度图像幅值
//*************************************************************
void SobelAmplitude(const Mat imageGradX,const Mat imageGradY,Mat &SobelAmpXY)
{
 SobelAmpXY=Mat::zeros(imageGradX.size(),CV_32FC1);
 for(int i=0;i<SobelAmpXY.rows;i++)
 {
  for(int j=0;j<SobelAmpXY.cols;j++)
  {
   SobelAmpXY.at<float>(i,j)=sqrt(imageGradX.at<uchar>(i,j)*imageGradX.at<uchar>(i,j)+imageGradY.at<uchar>(i,j)*imageGradY.at<uchar>(i,j));
  }
 }
 convertScaleAbs(SobelAmpXY,SobelAmpXY);
}
//******************局部极大值抑制*************************
//第一个参数imageInput输入的Sobel梯度图像;
//第二个参数imageOutPut是输出的局部极大值抑制图像;
//第三个参数pointDrection是图像上每个点的梯度方向数组指针
//*************************************************************
void LocalMaxValue(const Mat imageInput,Mat &imageOutput,double *pointDrection)
{
 //imageInput.copyTo(imageOutput);
 imageOutput=imageInput.clone();
 int k=0;
 for(int i=1;i<imageInput.rows-1;i++)
 {
  for(int j=1;j<imageInput.cols-1;j++)
  {
   int value00=imageInput.at<uchar>((i-1),j-1);
   int value01=imageInput.at<uchar>((i-1),j);
   int value02=imageInput.at<uchar>((i-1),j+1);
   int value10=imageInput.at<uchar>((i),j-1);
   int value11=imageInput.at<uchar>((i),j);
   int value12=imageInput.at<uchar>((i),j+1);
   int value20=imageInput.at<uchar>((i+1),j-1);
   int value21=imageInput.at<uchar>((i+1),j);
   int value22=imageInput.at<uchar>((i+1),j+1);
 
   if(pointDrection[k]>0&&pointDrection[k]<=45)
   {
    if(value11<=(value12+(value02-value12)*tan(pointDrection[i*imageOutput.rows+j]))||(value11<=(value10+(value20-value10)*tan(pointDrection[i*imageOutput.rows+j]))))
    {
     imageOutput.at<uchar>(i,j)=0;
    }
   } 
   if(pointDrection[k]>45&&pointDrection[k]<=90)
 
   {
    if(value11<=(value01+(value02-value01)/tan(pointDrection[i*imageOutput.cols+j]))||value11<=(value21+(value20-value21)/tan(pointDrection[i*imageOutput.cols+j])))
    {
     imageOutput.at<uchar>(i,j)=0;
 
    }
   }
   if(pointDrection[k]>90&&pointDrection[k]<=135)
   {
    if(value11<=(value01+(value00-value01)/tan(180-pointDrection[i*imageOutput.cols+j]))||value11<=(value21+(value22-value21)/tan(180-pointDrection[i*imageOutput.cols+j])))
    {
     imageOutput.at<uchar>(i,j)=0;
    }
   }
   if(pointDrection[k]>135&&pointDrection[k]<=180)
   {
    if(value11<=(value10+(value00-value10)*tan(180-pointDrection[i*imageOutput.cols+j]))||value11<=(value12+(value22-value11)*tan(180-pointDrection[i*imageOutput.cols+j])))
    {
     imageOutput.at<uchar>(i,j)=0;
    }
   }
   k++;
  }
 }
}
 
//******************双阈值处理*************************
//第一个参数imageInput输入和输出的的Sobel梯度幅值图像;
//第二个参数lowThreshold是低阈值
//第三个参数highThreshold是高阈值
//******************************************************
void DoubleThreshold(Mat &imageIput,double lowThreshold,double highThreshold)
{
 for(int i=0;i<imageIput.rows;i++)
 {
  for(int j=0;j<imageIput.cols;j++)
  {
   if(imageIput.at<uchar>(i,j)>highThreshold)
   {
    imageIput.at<uchar>(i,j)=255;
   } 
   if(imageIput.at<uchar>(i,j)<lowThreshold)
   {
    imageIput.at<uchar>(i,j)=0;
   } 
  }
 }
}
//******************双阈值中间像素连接处理*********************
//第一个参数imageInput输入和输出的的Sobel梯度幅值图像;
//第二个参数lowThreshold是低阈值
//第三个参数highThreshold是高阈值
//*************************************************************
void DoubleThresholdLink(Mat &imageInput,double lowThreshold,double highThreshold)
{
 for(int i=1;i<imageInput.rows-1;i++)
 {
  for(int j=1;j<imageInput.cols-1;j++)
  {
   if(imageInput.at<uchar>(i,j)>lowThreshold&&imageInput.at<uchar>(i,j)<255)
   {
    if(imageInput.at<uchar>(i-1,j-1)==255||imageInput.at<uchar>(i-1,j)==255||imageInput.at<uchar>(i-1,j+1)==255||
     imageInput.at<uchar>(i,j-1)==255||imageInput.at<uchar>(i,j)==255||imageInput.at<uchar>(i,j+1)==255||
     imageInput.at<uchar>(i+1,j-1)==255||imageInput.at<uchar>(i+1,j)==255||imageInput.at<uchar>(i+1,j+1)==255)
    {
     imageInput.at<uchar>(i,j)=255;
     DoubleThresholdLink(imageInput,lowThreshold,highThreshold); //递归调用
    } 
    else
   {
     imageInput.at<uchar>(i,j)=0;
   }    
   }    
  }
 }
}
 

当然也可以调用库函数:

int main( )
{
 Mat src = imread("1.jpg"); 
 Mat src1=src.clone();
 imshow("src", src); 
 
 // 一、最简单的canny用法,拿到原图后直接用。
 /*opencv自带的canny函数有两种重载形式
 * 第一种:
 InputArray image, (输入图像:8-bit)
 OutputArray edges, (输出边缘图像:单通道,8-bit,size与输入图像一致)
 double threshold1, (阈值1)
 double threshold2, (阈值2)
 int apertureSize=3, (Sober算子大小)
 bool L2gradient=false (是否采用更精确的方式计算图像梯度)
 * 第二种
 InputArray dx, (输入图像在x方向的导数:16-bit(CV_16SC1或CV_16SC3))
 InputArray dy, (输入图像在y方向的导数:16-bit(CV_16SC1或CV_16SC3))
 OutputArray edges, (输出边缘图像:单通道,8-bit,size与输入图像一致)
 double threshold1, (阈值1)
 double threshold2, (阈值2)
 bool L2gradient=false (是否采用更精确的方式计算图像梯度)  
 * 需要注意的是:低于阈值1的像素点会被认为不是边缘;高于阈值2的像素点会被认为是边缘;在阈值1和阈值2之间的像素点,若与第2步得到的边缘像素点相邻,则被认为是边缘,否则被认为不是边缘。
 * L2gradient:true计算梯度时用平方和平方根,false则是绝对值的和
 */
 Canny( src, src, 150, 100,3 );
 imshow("【效果图】Canny边缘检测", src); 
 // 二、高阶的canny用法,转成灰度图,降噪,用canny,最后将得到的边缘作为掩码,拷贝原图到效果图上,得到彩色的边缘图
 Mat dst,edge,gray;
 dst.create( src1.size(), src1.type() );
 cvtColor( src1, gray, CV_BGR2GRAY );
 blur( gray, edge, Size(3,3) );//最好使用高斯滤波
 Canny( edge, edge, 3, 9,3 );
 dst = Scalar::all(0);
 src1.copyTo( dst, edge);
 imshow("dst", dst); 
 waitKey(0); 
}

欢迎扫描二维码关注微信公众号 深度学习与数学 [每天获取免费的大数据、AI等相关的学习资源、经典和最新的深度学习相关的论文研读,算法和其他互联网技能的学习,概率论、线性代数等高等数学知识的回顾]

9518993b-6c13-eb11-8da9-e4434bdf6706.png
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值