zoukankan      html  css  js  c++  java
  • OpenCV 2.4+ C++ 边缘梯度计算

    图像的边缘

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

    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, intborderType=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, intdtype=-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。不同的通道是独立处理的。

    被山寨的原文

    Sobel Derivatives . OpenCV.org

    Image Filtering . OpenCV.org

  • 相关阅读:
    WebRTC Native开发实战之数据采集--摄像头
    windows上实现锁屏和息屏
    在ubuntu上使用v4l2loopback和ffmpeg模拟摄像头
    webrtc统计信息之rtt计算
    Window上利用windbg查看dmp文件崩溃堆栈
    组合模式(C++)
    外观模式(C++)
    单例模式(C++)
    桥接模式(C++)
    装饰器模式(C++)
  • 原文地址:https://www.cnblogs.com/justany/p/2782660.html
Copyright © 2011-2022 走看看