OpenCV从入门到入魔(4):图像边缘处理与非线性滤波(中值、双边)

460 阅读9分钟

OpenCV4Android开发实录(4):图像去噪与线性滤波(均值、方框、高斯)文章中,我们较为详细地介绍了OpenCV中几种常用的线性滤波方法原理和相关API的使用,本文将在此基础上继续讲解OpenCV中中值和双边两种非线性滤波,以及在图像滤波过程中OpenCV对图像边缘的处理方法。

1. 非线性滤波

 由上一篇文章可知,线性滤波器考虑的是每个像素的输出值为其邻域像素的加权和,这种滤波器易于构造,且易于从频率响应角度来进行分析。然而,在很多情况下,比如图像中偶尔出现很大的值时(散粒噪声),如果使用高斯滤波器对图像进行模糊时,噪声像素并没有被去除,它们只是变得柔和但仍然可见散粒,同时也带来了图像细节模糊,这时候应该使用相关的非线性滤波器替代线性滤波器来处理。

1.1 中值滤波

1. 基本理论中值滤波是基于排序统计理论的一种能够有效抑制噪声的非线性信号处理的非线性滤波技术,它的基本原理是用像素点邻域灰度值的中值来替代该像素点的灰度值,以便让周围的像素值接近真实值,从而消除孤立点的噪声点和保留图像细节信息。由于中值滤波不依赖于邻域内与要处理像素真实值相差很大的值(噪声像素),中值滤波对处理斑点噪声和椒盐噪声非常有效。中值滤波器在处理连续图像窗函数时与线性滤波器的工作方式类似,但滤波过程不再是邻域像素的加权运算。
假设取核大小为3x3,取源图像一块区域进行中值滤波所得结果,即先对该区域目标像素值排序,然后将目标像素(原始值为33)使用该区域像素值中间值代替,即52。示意图如下: 这里写图片描述 2. 源码解析 (1)  medianBlu函数原型

// 中值滤波
// src:输入源图像,Mat类型
// dst:输出图像,Mat类型
// ksize:核大小
void medianBlur( InputArray src, OutputArray dst, int ksize );

(2) 源码解析
medianBlur函数源码位于...\openCV3.3.0\opencv\sources\modules\imgproc\src的smooth.cpp中,具体内容如下:

void cv::medianBlur( InputArray _src0, OutputArray _dst, int ksize )
{
    CV_INSTRUMENT_REGION()
    CV_Assert( (ksize % 2 == 1) && (_src0.dims() <= 2 ));
	// ksize小于1,或源图像无数据直接返回
    if( ksize <= 1 || _src0.empty() )
    {
        _src0.copyTo(_dst);
        return;
    }

    CV_OCL_RUN(_dst.isUMat(),
               ocl_medianFilter(_src0,_dst, ksize))
	// 目标Mat与输入图像大小、像素类型一致
    Mat src0 = _src0.getMat();
    _dst.create( src0.size(), src0.type() );
    Mat dst = _dst.getMat();

    CV_OVX_RUN(true,
               openvx_medianFilter(_src0, _dst, ksize))

    CV_IPP_RUN_FAST(ipp_medianFilter(src0, dst, ksize));

#ifdef HAVE_TEGRA_OPTIMIZATION
    if (tegra::useTegra() && tegra::medianBlur(src0, dst, ksize))
        return;
#endif
	// 当核大小为3x3,或者5x5且源图像位深大于CV_8U或通道数量为2时
	// useSortNet=true
    bool useSortNet = ksize == 3 || (ksize == 5
#if !(CV_SSE2 || CV_NEON)
            && ( src0.depth() > CV_8U || src0.channels() == 2 || src0.channels() > 4 )
#endif
        );

    Mat src;
    // 以下代码开始进行中值滤波处理
    // 当useSortNet=true,根据源图像的深度调用medianBlur_SortNet函数使用不同
    // 的模板(核)大小进行中值滤波操作
    if( useSortNet )
    {
        if( dst.data != src0.data )
            src = src0;
        else
            src0.copyTo(src);

        if( src.depth() == CV_8U )
            medianBlur_SortNet<MinMax8u, MinMaxVec8u>( src, dst, ksize );
        else if( src.depth() == CV_16U )
            medianBlur_SortNet<MinMax16u, MinMaxVec16u>( src, dst, ksize );
        else if( src.depth() == CV_16S )
            medianBlur_SortNet<MinMax16s, MinMaxVec16s>( src, dst, ksize );
        else if( src.depth() == CV_32F )
            medianBlur_SortNet<MinMax32f, MinMaxVec32f>( src, dst, ksize );
        else
            CV_Error(CV_StsUnsupportedFormat, "");

        return;
    }
    // 当useSortNet=false情况
    else
    {
	    // 使用BORDER_REPLICATE方法处理图像边缘
        cv::copyMakeBorder( src0, src, 0, 0, ksize/2, ksize/2, BORDER_REPLICATE );

        int cn = src0.channels();
        CV_Assert( src.depth() == CV_8U && (cn == 1 || cn == 3 || cn == 4) );

        double img_size_mp = (double)(src0.total())/(1 << 20);
        // 根据条件调用合适方法进行中值滤波,这些方法实现的是具体的中值滤波算法
        // 这里我们就不继续看进去了
        if( ksize <= 3 + (img_size_mp < 1 ? 12 : img_size_mp < 4 ? 6 : 2)*
            (MEDIAN_HAVE_SIMD && (checkHardwareSupport(CV_CPU_SSE2) || checkHardwareSupport(CV_CPU_NEON)) ? 1 : 3))
            medianBlur_8u_Om( src, dst, ksize );
        else
            medianBlur_8u_O1( src, dst, ksize );
    }
}

3. 实战演练

void ImageSmoothing::mediaBlurImage(Mat srcImage, Mat &dstImage, int ksize) {
	if (ksize <= 0) {
		printf("ksize should be > 0, and the best to be odd number like 1,3,5,7...\n");
		return;
	}
	if (!srcImage.data) {
		printf("open source image error!\n");
		return;
	}
	medianBlur(srcImage, dstImage, ksize);
}

// 添加椒盐噪声,n为噪声点数量
void addSaltNoise(Mat &image, int n){
	// 添加白点
	for (int k = 0; k<n; k++)
	{
		int i = rand() % image.cols;
		int j = rand() % image.rows;
		if (image.channels() == 1){
			image.at<uchar>(j, i) = 255;
		}
		else if (image.channels() == 3){
			image.at<Vec3b>(j, i)[0] = 255;
			image.at<Vec3b>(j, i)[1] = 255;
			image.at<Vec3b>(j, i)[2] = 255;
		}
	}
	// 添加黑点
	for (int k = 0; k<n; k++)
	{
		int i = rand() % image.cols;
		int j = rand() % image.rows;
		if (image.channels() == 1) {
			image.at<uchar>(j, i) = 0;
		}
		else if (image.channels() == 3) {
			image.at<Vec3b>(j, i)[0] = 0;
			image.at<Vec3b>(j, i)[1] = 0;
			image.at<Vec3b>(j, i)[2] = 0;
		}
	}
}

效果演示:

  • 高斯滤波处理效果
    这里写图片描述

  • 中值滤波处理效果 这里写图片描述

 从两个结果来看,中值滤波对椒盐噪声的处理效果远好于高斯滤波(其他线性滤波器效果一样的),它不仅消除了图像中的椒盐噪声,同时能够有效地保护图像细节(包括图像边缘信息)。虽然中值滤波相较线性滤在处理这类噪声优势非常明显,但不可否认的是中值滤波花费的时间要高于线性滤波,这是因为中值滤波在处理每个像素时需要对包含该像素在内的邻域像素的灰度值进行排序,得到所有像素点灰度值的中值。
总而言之,中值滤波在一定条件下能够克服线性滤波器所带来的图像细节模糊,尤其对椒盐类似噪声非常有效,但是对一些图像细节很多的图像仍然不合适。

1.2 双边滤波

1. 基本理论双边滤波(Bilateral filter)是一种非线性的滤波方法,是结合图像的空间邻近度和像素值相似度的一种折衷处理,同时考虑空域信息和灰度相似性,达到保边去噪的目的。具有简单、非迭代、局部的特点。双边滤波器的好处是可以做边缘保存(edge preserving),一般过去用的维纳滤波或者高斯滤波去降噪,都会较明显地模糊边缘,对于高频细节的保护效果并不明显。双边滤波器顾名思义比高斯滤波多了一个高斯方差sigma-d,它是基于空间分布的高斯滤波函数,所以在边缘附近,离的较远的像素不会太多影响到边缘上的像素值,这样就保证了边缘附近像素值的保存。但是由于保存了过多的高频信息,对于彩色图像里的高频噪声,双边滤波器不能够干净的滤掉,只能够对于低频信息进行较好的滤波。
在双边滤波器中,输出像素的值依赖于邻域像素值的加权值组合,公式如下:
这里写图片描述
其中,w(i,j,k,l)是滤波器的加权系数,即核,取决于定义域核和值域核的乘积。 (1) 定义核域 这里写图片描述 (2)值域核
这里写图片描述
双边滤波处理示意图: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XNFITTLC-1638367297384)(p0.so.qhimgs1.com/t018b744f43…)] 2. 源码解析 (1)  bilateralFilter函数原型

// 双边滤波
// _src:源图像,只支持像素类型为CV_8U和CV_32F 
// _dst:目标图像,图像尺寸和类型与源图像一致
// d:滤波核直径
// sigmaColor:双边滤波核高斯系数标准差
// sigmaSpace:/双边滤波核空间域系数标准差
// borderType:指定图像边缘方法,默认为BORDER_DEFAULT
void cv::bilateralFilter( InputArray _src, OutputArray _dst, int d,
                      double sigmaColor, double sigmaSpace,
                      int borderType )

(2) 源码解析
bilateralFilter函数源码位于...\openCV3.3.0\opencv\sources\modules\imgproc\src的smooth.cpp中,具体内容如下:

void cv::bilateralFilter( InputArray _src, OutputArray _dst, int d,
                      double sigmaColor, double sigmaSpace,
                      int borderType )
{
    CV_INSTRUMENT_REGION()
	// 初始化dst Mat
    _dst.create( _src.size(), _src.type() );

    CV_OCL_RUN(_src.dims() <= 2 && _dst.isUMat(),
               ocl_bilateralFilter_8u(_src, _dst, d, sigmaColor, sigmaSpace, borderType))

    Mat src = _src.getMat(), dst = _dst.getMat();

    CV_IPP_RUN_FAST(ipp_bilateralFilter(src, dst, d, sigmaColor, sigmaSpace, borderType));
	// 双边滤波只支持CV_8U和CV_32F 两种类型图像
	// 通过调用算法实现函数bilateralFilter_8u和bilateralFilter_32f实现
    if( src.depth() == CV_8U )
        bilateralFilter_8u( src, dst, d, sigmaColor, sigmaSpace, borderType );
    else if( src.depth() == CV_32F )
        bilateralFilter_32f( src, dst, d, sigmaColor, sigmaSpace, borderType );
    else
        CV_Error( CV_StsUnsupportedFormat,
        "Bilateral filtering is only implemented for 8u and 32f images" );
}

3. 实战演练

void ImageSmoothing::bilateralFilterImage(Mat srcImage, Mat &dstImage, int d, double sigmaColor, double sigmaSpace) {
	if (!srcImage.data) {
		printf("open source image error!\n");
		return;
	}
	bilateralFilter(srcImage, dstImage, d, sigmaColor, sigmaSpace);
}

效果图
这里写图片描述

2. 图像边缘处理

 经过了解,无论是线性滤波还是非线性滤波,它们均有一个特点,即利用窗口函数(或称核、模板)依次扫描图像上的像素,然后根据相应的线性滤波或非线性滤波的规则计算得出被平滑处理像素的值,直到扫描完整副图像为止。扫描的过程如下图所示: 这里写图片描述  上图演示的是核大小为3x3、锚点为核中心位置时图像滤波处理的过程,可以看出原始图像的边缘有一个像素没有被处理,且这个未处理的边缘像素数量是随核增大而变大的,而未被处理的图像边缘像素信息将被丢失。下图为核大小为5x5图像滤波演示图,可以发现图像边缘有两个像素未被处理。 这里写图片描述

 OpenCV图像边缘处理方法

 不知大家在调用OpenCV线性和非线性相关滤波函数API时,是否发现它们的参数列表中均需传入一个int类型的borderType参数,且默认值为BORDER_DEFAULT。事实上,这个参数就是OpenCV提供用于指定如何去处理图像边缘的方法,除了BORDER_DEFAULT类型,还包括BORDER_CONSTANT、BORDER_REPLICATE以及BORDER_WRAP,它们处理的原理是在卷积开始之前增加边缘像素,填充的像素值为0或者RGB黑色,比如3x3在四周各填充1个像素的边缘,这样就确保图像的边缘被处理,在卷积处理之后再去掉这些边缘。 1. copyMakeBorder函数讲解

// 扩充src的边缘将图像变大,然后以各种外插方式自动填充图像边界
// 	src:源图像,Mat类型
//  dst:输出图像,Mat类型
//  top、bottom、left、right:图像边缘增加的像素数量
//  borderType:指定边缘处理的方法,一共有四种
//  value:颜色值。当borderType=BORDER_CONSTANT设置边缘像素的颜色值
void copyMakeBorder(InputArray src, OutputArray dst,
             int top, int bottom, int left, int right,
             int borderType, const Scalar& value = Scalar() );

 图像边缘处理常见方法
(1) BORDER_DEFAULT:即BORDER_REFLECT_101,以最边缘像素为轴对称;  (2) BORDER_CONSTANT:常量法,使用固定颜色value来填充;  (3) BORDER_REPLICATE :复制法,复制最边缘像素来填充  (4) BORDER_WRAP:对称法,用另外一边的像素来补偿填充; 2. 代码实现

void testCopyMakeBorder(int type) {
	Mat src, dst;
	src = imread("hashiqi.jpg");
	if (!src.data) {
		printf("加载源图像失败");
	}
	namedWindow("源图像");
	imshow("源图像", src);
	// 要处理边缘像素数量(测试用,可自定义)
	int top = (int)(0.05*src.rows);
	int bottom = (int)(0.05*src.rows);
	int left = (int)(0.05*src.cols);
	int right = (int)(0.05*src.cols);
	// 边缘处理,固定填充颜色为蓝色(OpenCV默认为BGR)
	copyMakeBorder(src, dst, top, bottom, left, right, type, 
			Scalar(255,0,0));
	imshow("BORDER_DEFAULT效果",dst);
}

 (1) BORDER_DEFAULT效果:
这里写图片描述  (2) BORDER_CONSTANT效果:
这里写图片描述  (3) BORDER_REPLICATE效果:
这里写图片描述  (4) BORDER_WRAP效果:
这里写图片描述

Android Studio代码(Java): OpenCV4Android Visual Studio代码(C++):OpenCVImageProc