在图像处理中经常有一个需求就是要知道图像中物体的边缘,以此来做物体区分或作其他处理,有时还可实现某些滤镜效果例如我们可以将图片转化成简笔画的效果。今天就用OpenCV的方法来实现以上提到的效果。OpenCV中可以实现边缘检测的方法有以下几种:
一、Canny算子
Canny边缘检测算子是John F.Canny于 1986 年开发出来的一个多级边缘检测算法。更为重要的是 Canny 创立了边缘检测计算理论,解释了这项技术是如何工作的。Canny边缘检测算法以Canny的名字命名,被很多人推崇为当今最优的边缘检测的算法。其内部已经做了降噪处理防止噪声干扰。
@param image 输入图像,即源图像,填Mat类的对象即可,且需为单通道8位图像。
@param edges 输出的边缘图,需要和源图片有一样的尺寸和类型。
@param threshold1 第一个滞后性阈值。
@param threshold2 第二个滞后性阈值。
@param apertureSize 表示应用Sobel算子的孔径大小,其有默认值3。
@param L2gradient 一个计算图像梯度幅值的标识,有默认值false。
CV_EXPORTS_W void Canny( InputArray image, OutputArray edges,
double threshold1, double threshold2,
int apertureSize = 3, bool L2gradient = false );
上面就是Canny方法的声明和参数说明,对于Canny算子的算法规则这里不进行讲解(后续可能会加上),这里只做用法的介绍。下面是一段边缘检测代码:
- (UIImage *)testCannyImage{
// 获取测试用的图片路径
NSString * path = [[NSBundle mainBundle] pathForResource:@"test" ofType:nil];
// 读取图片
cv::Mat testImage = cv::imread([path cStringUsingEncoding:NSUTF8StringEncoding]);
cv::Mat cannyImage;
// 检测图像边缘
cv::Canny(testImage, cannyImage, 90, 270);
// 转化色彩空间,因数OpenCV中图片默认是BGR而我们常用的是RGB.
cv::cvtColor(cannyImage, cannyImage, cv::COLOR_BGR2RGB);
return MatToUIImage(cannyImage);
}
上图的左边是原图右边是获取到的边缘图。从边缘图我们可以看出Canny算子处理后的效果像一幅钢笔画,线条有强烈的明暗对比且很硬朗。我们做一下简单处理就可以出现很好的艺术效果,我们先将原图作一定的模糊处理,再将边缘图和原图合在一起就可以实现漫画的艺术效果,可以将其作为一种滤镜来使用(好像在各大短视频应用中已经有这种滤镜了,但实现方法应该不是用的OpenCV因为OpenCV在iOS设置上无法使用GPU加速)代码下如。
- (UIImage *)testCannyImage{
// 获取测试用的图片路径
NSString * path = [[NSBundle mainBundle] pathForResource:@"test" ofType:nil];
// 读取图片
cv::Mat testImage = cv::imread([path cStringUsingEncoding:NSUTF8StringEncoding]);
cv::Mat cannyImage;
// 获取图像边缘
cv::Canny(testImage, cannyImage, 90, 270);
cv::Mat colorImage;
// 设置图片的大小和类型的图片
colorImage.create(testImage.rows, testImage.cols, testImage.type());
// 将图片内容设为的值都设置为0(即黑色)
colorImage = cv::Scalar(0);
// 将图像反转(即黑变白、白变黑)
cv::bitwise_not(cannyImage , cannyImage);
// 加入高斯模糊
cv::GaussianBlur(testImage, testImage, cv::Size(7,7), 1);
// 将边缘图做为掩码把原图拷贝到colorImage上
testImage.copyTo(colorImage, cannyImage);
// 转化色彩空间,因数OpenCV中图片默认是BGR而我们常用的是RGB.
cv::cvtColor(colorImage, colorImage, cv::COLOR_BGR2RGB);
return MatToUIImage(colorImage);
}
对于边缘图能实现的艺术效果不限于此,通过参数和步骤的不同可以实现不同的艺术效果,这里就先介绍这么多,如果有想法可以试试实现其他效果。
二、Sobel算子
Sobel算子结合了高斯平滑和微分求导,用来计算图像灰度函数的近似梯度。该方法是存在方向的,一般要求出X方向和Y方向的两个结果图来合成一个最终的结果图。方法及参数如下:
@param src 为输入图像,填Mat类型即可。
@param dst 即目标图像,函数的输出参数,需要和源图片有一样的尺寸和类型。
@param ddepth 输出图像的深度。
@param dx x 方向上的差分阶数。
@param dy y方向上的差分阶数。
@param ksize 有默认值3,表示Sobel核的大小;必须取1,3,5或7。
@param scale 计算导数值时可选的缩放因子,默认值是1,表示默认情况下是没有应用缩放的。我们可以在文档中查阅getDerivKernels的相关介绍,来得到这个参数的更多信息。
@param delta 表示在结果存入目标图(第二个参数dst)之前可选的delta值,有默认值0。
@param borderType 我们的老朋友了(万年是最后一个参数),边界模式,默认值为BORDER_DEFAULT。这个参数可以在官方文档中borderInterpolate处得到更详细的信息。
CV_EXPORTS_W 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 );
Soble算子出来的边缘图是不明暗变化的,它更像是一个素描画有明暗的变化。我们先做一个边缘画,实现代码如下。
- (UIImage *)testSobelImage{
// 获取测试用的图片路径
NSString * path = [[NSBundle mainBundle] pathForResource:@"test" ofType:nil];
// 读取图片
cv::Mat testImage = cv::imread([path cStringUsingEncoding:NSUTF8StringEncoding]);
// 声明三个图像头信息
cv::Mat sobelX;
cv::Mat sobelY;
cv::Mat sobelImage;
// 获取X和Y方向的边缘图
cv::Sobel(testImage, sobelX, CV_8U, 1, 0);
cv::Sobel(testImage, sobelY, CV_8U, 0, 1);
// 合并X和Y方向上的边缘图
cv::addWeighted(sobelX, 0.5, sobelY, 0.5, 0, sobelImage);
// 转化色彩空间,因数OpenCV中图片默认是BGR而我们常用的是RGB.
cv::cvtColor(sobelImage, sobelImage, cv::COLOR_BGR2RGB);
return MatToUIImage(sobelImage);
}
我们可以看到Sobel算子处理后的图与原图有很大的关系,原图是彩色的结果就是彩色的这与Canny算子不同,Canny算子不管原图是什么结果都是灰图。从结果图中我们还可以看到其线条并不是那么明暗分明,有渐变过渡更像是一幅铅笔的素描画。我们将其中的色彩去掉并反转彩色让其变成一幅素描图,实现代码如下。
- (UIImage *)testSobelImage{
// 获取测试用的图片路径
NSString * path = [[NSBundle mainBundle] pathForResource:@"test" ofType:nil];
// 读取图片
cv::Mat testImage = cv::imread([path cStringUsingEncoding:NSUTF8StringEncoding]);
// 将新图转成灰图
cv::cvtColor(testImage, testImage, cv::COLOR_BGR2GRAY);
// 声明三个图像头信息
cv::Mat sobelX;
cv::Mat sobelY;
cv::Mat sobelImage;
// 获取X和Y方向的边缘图
cv::Sobel(testImage, sobelX, CV_8U, 1, 0);
cv::Sobel(testImage, sobelY, CV_8U, 0, 1);
// 合并X和Y方向上的边缘图
cv::addWeighted(sobelX, 0.5, sobelY, 0.5, 0, sobelImage);
// 将图像反转(即黑变白、白变黑)
cv::bitwise_not(sobelImage, sobelImage);
return MatToUIImage(sobelImage);
}
上图就是一个素描图,不过看上去却有点浮雕效果。
三、Laplacian算子
Laplacian函数可以计算出图像经过拉普拉斯变换后的结果。Laplacian函数其实主要是利用sobel算子的运算。它通过加上sobel算子运算出的图像x方向和y方向上的导数,来得到我们载入图像的拉普拉斯变换结果。其他方法及参数如下:
@param src 输入图像,即源图像,填Mat类的对象即可,且需为单通道8位图像。
@param dst 输出的边缘图,需要和源图片有一样的尺寸和通道数。
@param ddepth 目标图像的深度。
@param ksize 用于计算二阶导数的滤波器的孔径尺寸,大小必须为正奇数,且有默认值1。
@param scale 计算拉普拉斯值的时候可选的比例因子,有默认值1。
@param delta 表示在结果存入目标图(第二个参数dst)之前可选的delta值,有默认值0。
@param borderType 边界模式,默认值为BORDER_DEFAULT。这个参数可以在官方文档中borderInterpolate()处得到更详细的信息。
CV_EXPORTS_W void Laplacian( InputArray src, OutputArray dst, int ddepth,
int ksize = 1, double scale = 1, double delta = 0,
int borderType = BORDER_DEFAULT );
我们实现一个边缘图,代码如下:
- (UIImage *)testLaplacianImage{
// 获取测试用的图片路径
NSString * path = [[NSBundle mainBundle] pathForResource:@"pp.jpg" ofType:nil];
// 读取图片
cv::Mat testImage = cv::imread([path cStringUsingEncoding:NSUTF8StringEncoding]);
cv::Mat laplacianImage;
// 获取边缘图
cv::Laplacian(testImage, laplacianImage, CV_8U);
// 转化色彩空间,因数OpenCV中图片默认是BGR而我们常用的是RGB.
cv::cvtColor(laplacianImage, laplacianImage, cv::COLOR_BGR2RGB);
return MatToUIImage(laplacianImage);
}
可以看到,经过Laplacian处理的边缘图比Sobel算子处理的边缘图弱了很多,如果不注意很难发现图中的内容。我们同样将其转成一个素描图看看效果是什么样,代码如下:
- (UIImage *)testLaplacianImage{
// 获取测试用的图片路径
NSString * path = [[NSBundle mainBundle] pathForResource:@"pp.jpg" ofType:nil];
// 读取图片
cv::Mat testImage = cv::imread([path cStringUsingEncoding:NSUTF8StringEncoding]);
cv::Mat laplacianImage;
// 获取边缘图
cv::Laplacian(testImage, laplacianImage, CV_8U);
// 转化色彩空间,因数OpenCV中图片默认是BGR而我们常用的是RGB.
cv::cvtColor(laplacianImage, laplacianImage, cv::COLOR_BGR2GRAY);
// 将图像反转(即黑变白、白变黑)
cv::bitwise_not(laplacianImage, laplacianImage);
return MatToUIImage(laplacianImage);
}
上图是拉普拉斯处理的素描图我们可以看到其比sobel算子处理的效果也弱很多。
四、Scharr
scharr一般我们称它为滤波器,而不是算子。通常其是配合Sobel算子一起使用的。scharr和Sobel一样也是有方向的,其实它的参数变量和Sobel基本上是一样的,除了没有ksize核的大小。可以参照Sobel学习效果没有Sobel,这里就不再重复了。