我们抠图的过程其实就是找图像中某个实例(例如人)的边缘的过程,所以,我们的边缘检测,其实就是检测图像中的实例的边缘。
那现在就有一个问题了,我们人是怎么区分边缘的呢?
我们发现,每一个实例,它的边缘跟其周围的像素差距一般是比较大的。我们的抠图,就是根据明显的像素差距,来区分实例的边缘。
所以抠图的根据,就是图像像素的明显变化。图像的边缘,一般都是图像上像素有明显变化的位置。
图像像素
我们发现,图像像素明显变化的位置,图像的倾斜程度都很大,也就是说斜率的绝对值很大。这和我们之前讲的几个算子的原理是一致的,Sobel算子,就是计算的这个位置:
边缘检测在书上的定义如下:
边缘检测是图像处理和计算机视觉中的基本问题,边缘检测的目的是标识数字图像中亮度变化明显的点。图像属性中的显著变化通常反映了属性的重要事件和变化。
Canny边缘检测算子是John F. Canny于 1986 年开发出来的一个多级边缘检测算法。它包括如下五个步骤:
1.应用高斯滤波来平滑图像,目的是去除噪声
2.灰度转化
3.找寻图像的强度梯度(intensity gradients),Sobel、Scharr
4.应用非最大抑制(non-maximum suppression)技术来消除边误检(本来不是但检测出来是)
5.应用双阈值的方法来决定可能的(潜在的)边界
- 5.1.利用滞后技术来跟踪边界
依次介绍
图像平滑就是让图像之间的像素差距更小一些,能够更好地提取特征更加明显的边缘,所以图像平滑的目的就是去除部分噪声。
而图像平滑操作,我们思考一下,不就是让挨着的像素之间差距变小,图像模糊的作用不就是这样吗?所以图像平滑操作就是进行图像模糊操作,而我们最常用的模糊操作,是高斯模糊。
cv::cvtColor
Canny算法的基本思想是寻找一幅图像中灰度强度变化最强的位置。所谓变化最强,即指梯度方向。平滑后的图像中每个像素点的梯度可以由Sobel算子(一种卷积运算)来获得(opencv中有封装好的函数,可以求图像中每个像素点的n阶导数)。首先,利用如下的核来分别求得沿水平(x)和垂直(y)方向的梯度G_X和G_Y。
Sobel和Scharr
为了消除边误检
这一步的目的是将模糊的边界变得清晰(sharp)。通俗的讲,就是保留了每个像素点上梯度强度的极大值,而删掉其他的值。对于每个像素点,进行如下操作:
a) 将其梯度方向近似为以下值中的一个:
(0,45,90,135,180,225,270,315)(即上下左右和45度方向)
b) 比较该像素点,和其梯度方向正负方向的像素点的梯度强度
c) 如果该像素点梯度强度最大则保留,否则抑制(删除,即置为0)
为了更好的解释这个概念,看下图:
图中的数字代表了像素点的梯度强度,箭头方向代表了梯度方向。
以第二排第三个像素点为例,由于梯度方向向上,则将这一点的强度(7)与其上下两个像素点的强度(5和4)比较,由于这一点强度最大,则保留。
下面再次举例解释:
上一步梯度的计算,分别求出了x、y方向的梯度。那么我们就可以根据这个梯度来得出梯度的方向(角度)。
比如水平方向的梯度变化最大,那么我们就保留与它垂直的方向(距离最远),左右两侧都去掉
即:
0-22.5和157.5-180这两个区域内都视为水平方向,那么我们就比对它和它上下两个像素值, 如果上下两个像素值小于这个像素,那么上下都被舍弃,当前像素被保留; 如果上下两个像素值大于这个像素,那么当前像素就被舍弃
67.5-112.5区间内,视为水平,当前像素与左右相邻像素比对
112.5-157.5区间内,视为左上右下方向,当前像素与左下右上相邻像素比对
22.5-67.5区间内,视为左下右上方向,当前像素与左上右下相邻像素比对
经过非极大抑制后图像中仍然有很多噪声点。
Canny算法中应用了一种叫双阈值的技术。即设定一个阈值上界和阈值下界(opencv中通常由人为指定的)
阈值上界、阈值下界,也被称为高阈值、低阈值。
Canny算法建议高阈值:低阈值介于2:1和3:1之间。
下图是高:低为5:1和3:2对一张测试图案和照片处理的结果。
较高的亮度梯度比较有可能是边缘,亮度低于低阈值的一定不可能是边缘。
但是没有一个确切的值来限定介于高低阈值之间的亮度梯度是否是边缘,所以 Canny 使用了滞后阈值。
滞后阈值(Hysteresis thresholding)需要两个阈值,即高阈值与低阈值。
假设图像中的重要边缘都是连续的曲线,这样我们就可以跟踪给定曲线中模糊的部分,并且避免将没有组成曲线 的噪声像素当成边缘。所以我们从一个较大的阈值开始,这将标识出我们比较确信的真实边缘,使用前面导出的方向信息,我们从这些真正的边缘开始在图像中跟踪 整个的边缘。在跟踪的时候,我们使用一个较小的阈值,这样就可以跟踪曲线的模糊部分直到我们回到起点。
即:
当介于高低阈值间的边缘连接到一个高于阈值的像素时才被接受
void Canny(
InputArray src,
OutputArray edges,
double threshold1,
double threshold2,
int apertureSize = 3,
bool L2gradient = false
);
含义如下
(1)InputArray类型的src ,输入图像。
必须是单通道图像。(2)OutputArray类型的edges,输出边缘图;单通道8位图像,其大小与图像相同。
输出图像必须是灰度图(实际上是一副布尔图),一般让它背景为黑色(3)double类型的threshold1,滞后过程的第一个阈值—低阈值。
低阈值,常取高阈值的1/2或者1/3(4)double类型的threshold2,滞后过程的第二个阈值—高阈值。
高阈值(5)int类型的apertureSize,Sobel算子的Size,通常3x3,取值3。
(6)bool类型的L2gradient,一个标志,指示是否应使用更精确的L2来计算图像渐变幅度(L2gradient=true),或者是否应使用默认的L1(L2gradient=false)。(注:这里的L2是平方和开根号,L1是绝对值求和,如下图)
一般情况下用L1不用L2,默认false
#include
#include
using namespace std;
using namespace cv;
Mat src, gray, dst;
Mat output;
int t1_value = 50;
int max_value = 255;
void Canny_demo(int,void*)
{
blur(gray, gray, Size(3,3), Point(-1, -1), BORDER_DEFAULT);
Canny(gray, output, t1_value, t1_value*2, 3, false);
dst.create(src.size(), src.type());//将二值灰度图彩色化
src.copyTo(dst, output);//将源图像像素拷贝到二值图为白色的对应像素
imshow("bin img", output);
}
int main()
{
namedWindow("input img", CV_WINDOW_AUTOSIZE);
namedWindow("output img", CV_WINDOW_AUTOSIZE);
src = imread("C:/Users/86176/Pictures/pics/lena(1).tiff");
if (!src.data)
{
printf("fail to load img\n");
return -1;
}
imshow("input img", src);
cvtColor(src, gray, CV_BGR2GRAY);
createTrackbar("Threshold Value:", "output img", &t1_value, max_value, Canny_demo);
Canny_demo(0,0);
imshow("output img", dst);
waitKey(0);
return 0;
}