图像的边缘 html
图像的边缘从数学上是如何表示的呢? 数组
图像的边缘上,邻近的像素值应当显著地改变了。而在数学上,导数是表示改变快慢的一种方法。梯度值的大变预示着图像中内容的显著变化了。 app
用更加形象的图像来解释,假设咱们有一张一维图形。下图中灰度值的“跃升”表示边缘的存在: ide
使用一阶微分求导咱们能够更加清晰的看到边缘“跃升”的存在(这里显示为高峰值): 函数
由此咱们能够得出:边缘能够经过定位梯度值大于邻域的相素的方法找到。 ui
卷积 spa
卷积能够近似地表示求导运算。 .net
那么卷积是什么呢? code
卷积是在每个图像块与某个算子(核)之间进行的运算。 htm
核?!
核就是一个固定大小的数值数组。该数组带有一个锚点 ,通常位于数组中央。
但是这怎么运算啊?
假如你想获得图像的某个特定位置的卷积值,可用下列方法计算:
- 将核的锚点放在该特定位置的像素上,同时,核内的其余值与该像素邻域的各像素重合;
- 将核内各值与相应像素值相乘,并将乘积相加;
- 将所得结果放到与锚点对应的像素上;
- 对图像全部像素重复上述过程。
用公式表示上述过程以下:
![]()
在图像边缘的卷积怎么办呢?
计算卷积前,OpenCV经过复制源图像的边界建立虚拟像素,这样边缘的地方也有足够像素计算卷积了。
近似梯度
好比内核为3时。
首先对x方向计算近似导数:
而后对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_8U, ddepth = -1/CV_16S/CV_32F/CV_64F
- 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时, 输出图像将和输入图像有相同的深度。输入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位类型。
![]()
对于多通道矩阵,该函数对各通道独立处理。若是输出不是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 两个数组的加权和公式以下:
![]()
在多通道状况下,每一个通道是独立处理的,该函数能够被替换成一个函数表达式:
dst = src1*alpha + src2*beta + gamma;
利用convertScaleAbs和addWeighted,咱们能够对梯度进行一个能够用图像显示的近似表达。
这样咱们就能够获得下面的效果:
梯度方向
但有时候边界还不够,咱们但愿获得图片色块之间的关系,或者研究样本的梯度特征来对机器训练识别物体时候,咱们还须要梯度的方向。
二维平面的梯度定义为:
这很好理解,其代表颜色增加的方向与x轴的夹角。
但Sobel算子对于沿x轴和y轴的排列表示的较好,可是对于其余角度表示却不够精确。这时候咱们可使用Scharr滤波器。
Scharr滤波器的内核为:
这样能提供更好的角度信息,如今咱们修改原程序,改成使用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,意义等同于选择与第一个数组相同的深度。
该函数对两个数组进行除法:
![]()
或则只是缩放系数除以一个数组:
![]()
这种状况若是src2是0,那么dst也是0。不一样的通道是独立处理的。
被山寨的原文
Sobel Derivatives . OpenCV.org
Image Filtering . OpenCV.org