opencv 图像的边缘

图像的边缘

图像的边缘从数学上是如何表示的呢?

How intensity changes in an edge

图像的边缘上,邻近的像素值应当显著地改变了。而在数学上,导数是表示改变快慢的一种方法。梯度值的大变预示着图像中内容的显著变化了。

用更加形象的图像来解释,假设我们有一张一维图形。下图中灰度值的“跃升”表示边缘的存在:

    Intensity Plot for an edge

使用一阶微分求导我们可以更加清晰的看到边缘“跃升”的存在(这里显示为高峰值):

    First derivative of Intensity - Plot for an edge

由此我们可以得出:边缘可以通过定位梯度值大于邻域的相素的方法找到。

 

卷积

卷积可以近似地表示求导运算。

那么卷积是什么呢?

卷积是在每一个图像块与某个算子(核)之间进行的运算。

核?!

核就是一个固定大小的数值数组。该数组带有一个锚点 ,一般位于数组中央。

kernel example

 可是这怎么运算啊?

假如你想得到图像的某个特定位置的卷积值,可用下列方法计算:

  1. 将核的锚点放在该特定位置的像素上,同时,核内的其他值与该像素邻域的各像素重合;
  2. 将核内各值与相应像素值相乘,并将乘积相加;
  3. 将所得结果放到与锚点对应的像素上;
  4. 对图像所有像素重复上述过程。

用公式表示上述过程如下:

    H(x,y) = \sum_{i=0}^{M_{i} - 1} \sum_{j=0}^{M_{j}-1} I(x+i - a_{i}, y + j - a_{j})K(i,j)

在图像边缘的卷积怎么办呢?

计算卷积前,OpenCV通过复制源图像的边界创建虚拟像素,这样边缘的地方也有足够像素计算卷积了。

 

近似梯度

比如内核为3时。

首先对x方向计算近似导数:

G_{x} = \begin{bmatrix}-1 & 0 & +1  \\-2 & 0 & +2  \\-1 & 0 & +1\end{bmatrix} * I

然后对y方向计算近似导数:

G_{y} = \begin{bmatrix}-1 & -2 & -1  \\0 & 0 & 0  \\+1 & +2 & +1\end{bmatrix} * I

然后计算梯度:

G = \sqrt{ G_{x}^{2} + G_{y}^{2} }

当然你也可以写成:

G = |G_{x}| + |G_{y}|

 

开始求梯度

复制代码
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <stdlib.h>
#include <stdio.h>

using namespace cv;

int main( int argc, char** argv ){

    Mat src, src_gray;
    Mat grad;
    char* window_name = "求解梯度";
    int scale = 1;
    int delta = 0;
    int ddepth = CV_16S;

    int c;

    src = imread( argv[1] );

    if( !src.data ){ 
        return -1; 
    }

    //高斯模糊
    GaussianBlur( src, src, Size(3,3), 0, 0, BORDER_DEFAULT );

    //转成灰度图
    cvtColor( src, src_gray, CV_RGB2GRAY );

    namedWindow( window_name, CV_WINDOW_AUTOSIZE );

    Mat grad_x, grad_y;
    Mat abs_grad_x, abs_grad_y;

    Sobel( src_gray, grad_x, ddepth, 1, 0, 3, scale, delta, BORDER_DEFAULT );
    convertScaleAbs( grad_x, abs_grad_x );

    Sobel( src_gray, grad_y, ddepth, 0, 1, 3, scale, delta, BORDER_DEFAULT );
    convertScaleAbs( grad_y, abs_grad_y );

    addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad );

    imshow( window_name, grad );

    waitKey(0);

    return 0;
}
复制代码

 

Sobel函数

索贝尔算子(Sobel operator)计算。

C++:  void  Sobel (InputArray  src, OutputArray  dst, int  ddepth, int  dx, int  dy, int  ksize=3, double  scale=1, double  delta=0, int borderType=BORDER_DEFAULT  )
参数
  • src – 输入图像。
  • dst – 输出图像,与输入图像同样大小,拥有同样个数的通道。
  • ddepth –
    输出图片深度;下面是输入图像支持深度和输出图像支持深度的关系:
    • src.depth() = CV_8Uddepth = -1/CV_16S/CV_32F/CV_64F
    • src.depth() = CV_16U/CV_16Sddepth = -1/CV_32F/CV_64F
    • src.depth() = CV_32Fddepth = -1/CV_32F/CV_64F
    • src.depth() = CV_64Fddepth = -1/CV_64F

    当 ddepth为-1时, 输出图像将和输入图像有相同的深度。输入8位图像则会截取顶端的导数。

  • xorder – x方向导数运算参数。
  • yorder – y方向导数运算参数。
  • ksize – Sobel内核的大小,可以是:1,3,5,7。
  • scale – 可选的缩放导数的比例常数。
  • delta – 可选的增量常数被叠加到导数中。
  • borderType – 用于判断图像边界的模式。

代码注释:

//在x方向求图像近似导数
Sobel( src_gray, grad_x, ddepth, 1, 0, 3, scale, delta, BORDER_DEFAULT );

//在y方向求图像近似导数
Sobel( src_gray, grad_y, ddepth, 0, 1, 3, scale, delta, BORDER_DEFAULT );

如果我们打印上面两个输出矩阵,可以看到grad_x和grad_y中的元素有正有负。

当然,正方向递增就是正的,正方向递减则是负值。

这很重要,我们可以用来判断梯度方向。

 

convertScaleAbs函数

线性变换转换输入数组元素成8位无符号整型。

C++:  void  convertScaleAbs (InputArray  src, OutputArray  dst, double  alpha=1, double  beta=0 )
参数
  • src – 输入数组。
  • dst – 输出数组。
  • alpha – 可选缩放比例常数。
  • beta – 可选叠加到结果的常数。

对于每个输入数组的元素函数convertScaleAbs 进行三次操作依次是:缩放,得到一个绝对值,转换成无符号8位类型。

\texttt{dst} (I)= \texttt{saturate\_cast<uchar>} (| \texttt{src} (I)* \texttt{alpha} +  \texttt{beta} |)

对于多通道矩阵,该函数对各通道独立处理。如果输出不是8位,将调用Mat::convertTo 方法并计算结果的绝对值,例如:

Mat_<float> A(30,30);
randu(A, Scalar(-100), Scalar(100));
Mat_<float> B = A*5 + 3;
B = abs(B);

为了能够用图像显示,提供一个直观的图形,我们利用该方法,将-256 — 255的导数值,转成0 — 255的无符号8位类型。

 

addWeighted函数

计算两个矩阵的加权和。

C++:  void  addWeighted (InputArray  src1, double  alpha, InputArray  src2, double  beta, double  gamma, OutputArray  dst, int dtype=-1 )
参数
  • src1 – 第一个输入数组。
  • alpha – 第一个数组的加权系数。
  • src2 – 第二个输入数组,必须和第一个数组拥有相同的大小和通道。
  • beta – 第二个数组的加权系数。
  • dst – 输出数组,和第一个数组拥有相同的大小和通道。
  • gamma – 对所有和的叠加的常量。
  • dtype – 输出数组中的可选的深度,当两个数组具有相同的深度,此系数可设为-1,意义等同于选择与第一个数组相同的深度。

函数addWeighted 两个数组的加权和公式如下:

    \texttt{dst} (I)= \texttt{saturate} ( \texttt{src1} (I)* \texttt{alpha} +  \texttt{src2} (I)* \texttt{beta} +  \texttt{gamma} )

在多通道情况下,每个通道是独立处理的,该函数可以被替换成一个函数表达式:

    dst = src1*alpha + src2*beta + gamma;

利用convertScaleAbs和addWeighted,我们可以对梯度进行一个可以用图像显示的近似表达。

这样我们就可以得到下面的效果:

Result of applying Sobel operator to lena.jpg

 

梯度方向

但有时候边界还不够,我们希望得到图片色块之间的关系,或者研究样本的梯度特征来对机器训练识别物体时候,我们还需要梯度的方向。

二维平面的梯度定义为:

    

这很好理解,其表明颜色增长的方向与x轴的夹角。

但Sobel算子对于沿x轴和y轴的排列表示的较好,但是对于其他角度表示却不够精确。这时候我们可以使用Scharr滤波器。

Scharr滤波器的内核为:

    G_{x} = \begin{bmatrix}-3 & 0 & +3  \\-10 & 0 & +10  \\-3 & 0 & +3\end{bmatrix}G_{y} = \begin{bmatrix}-3 & -10 & -3  \\0 & 0 & 0  \\+3 & +10 & +3\end{bmatrix}

这样能提供更好的角度信息,现在我们修改原程序,改为使用Scharr滤波器进行计算:

复制代码
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <stdlib.h>
#include <stdio.h>

using namespace cv;

int main( int argc, char** argv ){

    Mat src, src_gray;
    Mat grad;
    char* window_name = "梯度计算";
    int scale = 1;
    int delta = 0;
    int ddepth = CV_16S;

    int c;

    src = imread( argv[1] );

    if( !src.data ){ 
        return -1; 
    }

    GaussianBlur( src, src, Size(3,3), 0, 0, BORDER_DEFAULT );

    cvtColor( src, src_gray, CV_RGB2GRAY );

    namedWindow( window_name, CV_WINDOW_AUTOSIZE );

    Mat grad_x, grad_y;
    Mat abs_grad_x, abs_grad_y;

    //改为Scharr滤波器计算x轴导数
    Scharr( src_gray, grad_x, ddepth, 1, 0, scale, delta, BORDER_DEFAULT );
    convertScaleAbs( grad_x, abs_grad_x );

    //改为Scharr滤波器计算y轴导数
    Scharr( src_gray, grad_y, ddepth, 0, 1, scale, delta, BORDER_DEFAULT );
    convertScaleAbs( grad_y, abs_grad_y );

    addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad );

    imshow( window_name, grad );

    waitKey(0);

    return 0;
}
复制代码

Scharr函数接受参数与Sobel函数相似,这里就不叙述了。

下面我们通过divide函数就能得到一个x/y的矩阵。

对两个输入数组的每个元素执行除操作。

C++:  void  divide (InputArray  src1, InputArray  src2, OutputArray  dst, double  scale=1, int  dtype=-1 )
C++:  void  divide (double  scale, InputArray  src2, OutputArray  dst, int  dtype=-1 )
参数
  • src1 – 第一个输入数组。
  • src2 – 第二个输入数组,必须和第一个数组拥有相同的大小和通道。
  • scale – 缩放系数。
  • dst – 输出数组,和第二个数组拥有相同的大小和通道。
  • dtype – 输出数组中的可选的深度,当两个数组具有相同的深度,此系数可设为-1,意义等同于选择与第一个数组相同的深度。

该函数对两个数组进行除法:

  \texttt{dst(I) = saturate(src1(I)*scale/src2(I))}

或则只是缩放系数除以一个数组:

  \texttt{dst(I) = saturate(scale/src2(I))}

这种情况如果src2是0,那么dst也是0。不同的通道是独立处理的。

 

#图像梯度 (注意都需要cv.convertScaleAbs将得到的有些负值取绝对值得到正数,并将数据转化到0-255之间,且sobel与Scarr算法中的数据位数都是32位浮型的) import cv2 as cv import numpy as np def sobel_demo(image): #注意是32位float数据 grad_x = cv.Scharr(image, cv.CV_32F, 1, 0) grad_y = cv.Scharr(image, cv.CV_32F, 0, 1) #当用sobel算子不能很好的得到边缘的时候,就可以用Scharr算子,这是加强版的sobel算子,就可以得到 #原图像不是很明显的边缘了 # grad_x =cv.Sobel(image,cv.CV_32F,1,0) # grad_y =cv.Sobel(image,cv.CV_32F,0,1) gradx =cv.convertScaleAbs(grad_x) grady = cv.convertScaleAbs(grad_y) #cv.imshow("gradx",gradx) #cv.imshow("grady",grady) dst = cv.addWeighted(gradx,0.5,grady,0.5,0) cv.imshow("sobel_demo",dst) def lapalace_demo(image): #dst =cv.Laplacian(image,cv.CV_32F) #dst =cv.convertScaleAbs(dst) 会把dst变成单通道的8位的0-255的图像 #也可以用filter2D来做拉普拉斯算子 kernel = np.array([[0,-1,0],[-1,4,-1],[0,-1,0]]) dst = cv.filter2D(image,cv.CV_32F,kernel) dst = cv.convertScaleAbs(dst) cv.imshow("lapalace",dst) src = cv.imread("E:/opencv/picture/step.jpg") cv.imshow("inital_window",src) #sobel_demo(src) lapalace_demo(src) cv.waitKey(0) cv.destroyAllWindows() 分析: 图像梯度可以把图像看成二维离散函数,图像梯度其实就是这个二维离散函数的求导。 一、 Sobel算子是普通一阶差分,是基于寻找梯度强度。拉普拉斯算子(二阶差分)是基于过零检测。通过计算梯度,设置阀值,得到边缘图像。 def sobel_demo(image): #注意是32位float数据 grad_x = cv.Scharr(image, cv.CV_32F, 1, 0) grad_y = cv.Scharr(image, cv.CV_32F, 0, 1) #当用sobel算子不能很好的得到边缘的时候,就可以用Scharr算子,这是加强版的sobel算子,就可以得到 #原图像不是很明显的边缘了 # grad_x =cv.Sobel(image,cv.CV_32F,1,0) # grad_y =cv.Sobel(image,cv.CV_32F,0,1) gradx = cv.convertScaleAbs(grad_x) grady = cv.convertScaleAbs(grad_y) #cv.imshow("gradx",gradx) #cv.imshow("grady",grady) dst = cv.addWeighted(gradx,0.5,grady,0.5,0) cv.imshow("sobel_demo",dst) 1.Sobel算子用来计算图像灰度函数的近似梯度。Sobel算子根据像素上下、左右邻灰度加权差,在边缘处达到极值这一现象检测边缘。对噪声具有平滑作用,提供较为精确的边缘方向信息,边缘定位精度不够高。当对精度要求不是很高时,是一种较为常用的边缘检测方法。 2.Sobel具有平滑和微分的功效。即:Sobel算子先将图像横向或纵向平滑,然后再纵向或横向差分,得到的结果是平滑后的差分结果。 OpenCV的Sobel函数原型为:Sobel(src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]]) -> dst 注:一般就设置src,ddepth = cv.CV_32F,dx,dy(对x方求梯度就是1,0对y方向求梯度就是0,1) src参数表示输入需要处理的图像。 ddepth参数表示输出图像深度,针对不同的输入图像,输出目标图像有不同的深度。   具体组合如下:   src.depth() = CV_8U, 取ddepth =-1/CV_16S/CV_32F/CV_64F (一般源图像都为CV_8U,为了避免溢出,一般ddepth参数选择CV_32F)   src.depth() = CV_16U/CV_16S, 取ddepth =-1/CV_32F/CV_64F   src.depth() = CV_32F, 取ddepth =-1/CV_32F/CV_64F   src.depth() = CV_64F, 取ddepth = -1/CV_64F   注:ddepth =-1时,代表输出图像与输入图像相同的深度。 dx参数表示x方向上的差分阶数,1或0 。 dy参数表示y 方向上的差分阶数,1或0 。 dst参数表示输出与src相同大小和相同通道数的图像。 ksize参数表示Sobel算子的大小,必须为1、3、5、7。 scale参数表示缩放导数的比例常数,默认情况下没有伸缩系数。 delta参数表示一个可选的增量,将会加到最终的dst中,同样,默认情况下没有额外的值加到dst中。 borderType表示判断图像边界的模式。这个参数默认值为cv2.BORDER_DEFAULT。 2.OpenCV的convertScaleAbs函数使用线性变换转换输入数组元素成8位无符号整型。函数原型:convertScaleAbs(src[, dst[, alpha[, beta]]]) -> dst src参数表示原数组。 dst参数表示输出数组 (深度为 8u)。 alpha参数表示比例因子。 beta参数表示原数组元素按比例缩放后添加的值。 3.OpenCV的addWeighted函数是计算两个数组的加权和。函数原型:addWeighted(src1, alpha, src2, beta, gamma[, dst[, dtype]]) -> dst 用于将x,y方向的梯度合成。 二、Scharr算子 当用sobel算子发现得到的边缘信息不明显的时候,就可以用Scharr算子了。该算子是sobel算子的加强版,用法也和sobel算子一样,效果更加突出。 Scharr算子也是计算x或y方向上的图像差分。OpenCV的Scharr函数原型为:Scharr(src, ddepth, dx, dy[, dst[, scale[, delta[, borderType]]]]) -> dst 参数和Sobel算子的几乎差不多,意思也一样,只是没有ksize大小。 三、拉普拉斯算子 1.拉普拉斯算子(Laplace Operator)是n维欧几里德空间中的一个二阶微分算子,定义为梯度(▽f)的散度(▽•f)。 2.OpenCV的Laplacian函数原型为:Laplacian(src, ddepth[, dst[, ksize[, scale[, delta[, borderType]]]]]) -> dst src参数表示输入需要处理的图像。 ddepth参数表示输出图像深度,针对不同的输入图像,输出目标图像有不同的深度。   具体组合如下:   src.depth() = CV_8U, 取ddepth =-1/CV_16S/CV_32F/CV_64F (一般源图像都为CV_8U,为了避免溢出,一般ddepth参数选择CV_32F) 注:当然我们也可以cv.filter2D命令来make一个拉普拉斯算子: kernel = np.array([[0,-1,0],[-1,4,-1],[0,-1,0]]) dst = cv.filter2D(image,cv.CV_32F,kernel) dst = cv.convertScaleAbs(dst) cv.imshow("lapalace",dst) Canny算法 canny 的目标是找到一个最优的边缘检测算法,最优边缘检测的含义是: 好的检测- 算法能够尽可能多地标识出图像中的实际边缘。 好的定位- 标识出的边缘要尽可能与实际图像中的实际边缘尽可能接近。 最小响应- 图像中的边缘只能标识一次,并且可能存在的图像噪声不应标识为边缘。 3.算法步骤:   ①高斯模糊 - GaussianBlur   ②灰度转换 - cvtColor   ③计算梯度 – Sobel/Scharr   ④非最大信号抑制   ⑤高低阈值输出二值图像 #canny算法常用步骤: #高斯模糊:因为canny对噪声很敏感,注意核size别太大 #源图像灰度化 #canny算法: 像素值小于低阈值的舍弃,高于高阈值的保留作为边缘信息,大于低阈值且该像素仅仅在连接到一个高于高阈值的像素时被保留。一般高阈值:低阈值在2:1到3:1之间 def canny_demo(image): image = cv.GaussianBlur(image,(3,3),0) gray = cv.cvtColor(image,cv.COLOR_BGR2GRAY) dst = cv.Canny(image,50,150) cv.imshow("Canny_demo",dst)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值