本章包括以下内容:
- 用低通滤波器进行图像滤波;
- 用滤波器进行缩减像素采样;
- 用中值滤波器进行图像滤波;
- 用定向滤波器检测边缘;
- 计算图像的拉普拉斯算子。
6.2 低通滤波器
本节将介绍几种非常基本的低通滤波器。这种滤波器的目的是减少图像变化的幅度。要做到这点,一个简单的方法是把每个像素的值替换成它周围像素的平均值。这样一来,强度的快速变化会被消除,取而代之的是更加平滑的过渡。
cv::blur 函数将每个像素的值替换成该像素邻域的平均值(邻域是矩形的),从而使图像更加平滑。这个低通滤波器的用法如下所示:
cv::blur(image,result, cv::Size(5,5)); // 滤波器尺寸
这是使用滤波器后得到的结果。
有时需要让邻域内较近的像素具有更高的重要度。因此可计算加权平均值,即较近的像素比较远的像素具有更大的权重。要得到加权平均值,可采用依据高斯函数(即“钟形曲线”函数)制定的加权策略。函数cv::GaussianBlur 应用了这种滤波器,调用方法如下所示:
cv::GaussianBlur(image, result,
cv::Size(5, 5), // 滤波器尺寸
1.5); // 控制高斯曲线形状的参数
得到的结果如下所示。
观察本节产生的输出图像,可以发现低通滤波器的最终效果是使图像更加模糊或更加平滑。这不奇怪,因为低通滤波器减弱了高频成分,而高频成分正好对应了物体边缘处的快速视觉变化。
对于高斯滤波器,像素对应的权重与它到中心像素之间的距离成正比。一维高斯函数的公式为:
使用归一化系数A 是为了确保高斯曲线下方的面积等于1。符号σ的值决定了高斯函数曲线的宽度。这个值越大,函数曲线就越扁平。
例如计算一维高斯滤波器的系数,区间[-4, 0, 4],如果σ = 0.5,得到如下系数:
[0.0 0.0 0.00026 0.10645 0.78657 0.10645 0.00026 0.0 0.0]
计算这些系数的方法是用对应的σ 的值调用函数cv::getGaussianKernel:
cv::Mat gauss= cv::getGaussianKernel(9, sigma,CV_32F);
6.3 用滤波器进行缩减像素采样
降低图像精度的过程称为缩减像素采样(downsampling),提升图像精度的过程称为提升像素采样(upsampling)。
要缩小一幅图像,可以用低通滤波器实现。因此在删除部分列和行之前,必须先在原始图像上应用低通滤波器,这样才能使图像在缩小到四分之一后不出现伪影。这是用OpenCV 的实现方法:
// 首先去除高频成分
cv::GaussianBlur(image, image, cv::Size(11, 11), 2.0);
// 只保留每4 个像素中的1 个
cv::Mat reduced(image.rows / 4, image.cols / 4, CV_8U);
for (int i = 0; i < reduced.rows; i++)
for (int j = 0; j < reduced.cols; j++)
reduced.at<uchar>(i, j) = image.at<uchar>(i * 4, j * 4);
OpenCV 中有一个专用函数,利用这个原理实现了图像缩减,即cv::pyrDown 函数:
cv::Mat reducedImage; // 用于存储缩小后的图像
cv::pyrDown(image,reducedImage); // 图像尺寸缩小一半
上述函数使用了一个5×5 的高斯滤波器,在把图像缩小一半之前先进行低通滤波。此外还有功能相反的函数cv::pyrUp,它可以放大图像的尺寸。
在这种提升像素采样的过程中,先在每两行和每两列之间分别插入值为0 的像素,然后对扩展后的图像应用同样的5×5 高斯滤波器(但系数要扩大4 倍)。先缩小一幅图像再把它放大,显然不能完全让它恢复到原始状态,因为缩小过程中丢失的信息是无法恢复的。
此外还有一个更通用的函数cv::resize,它可以指定缩放后图像的尺寸。你只需要在调用它时指定新的尺寸,这个尺寸可以比原始图像小,也可以比原始图像大:
cv::Mat resizedImage; // 用于存储缩放后的图像
cv::resize(image, resizedImage, cv::Size(image.cols/4,image.rows/4)); // 行和列均缩小为原来的1/4
你也可以指定缩放比例。在参数中提供一个空的图像实例,然后提供缩放比例:
cv::resize(image, resizedImage, cv::Size(), 1.0/4.0, 1.0/4.0); // 缩小为原来的1/4
像素插值
进行插值的最基本方法是使用最近邻策略。把待生成图像的像素网格放在原图像的上方,每个新像素被赋予原图像中最邻近像素的值。当图像升采样(即新网格比原始网格更密集时)时,会根据同一个原始像素,确定新网格中多个像素的值。例如要把缩小后的图像放大4 倍,采用最邻近插值法的代码如下所示:
cv::resize(reduced, newImage, cv::Size(), 3, 3, cv::INTER_NEAREST);
双线性插值方案是cv::resize 函数的缺省方法(可以用标志cv::INTER_LINEAR 显式地指定):
cv::resize(reduced, newImage, cv::Size(), 4, 4, cv::INTER_LINEAR);
此外还有一些算法,可以得到更好的结果。如果想使用双三次插值算法,就要在执行插值运算时考虑4×4 的邻域像素。但因为这种算法使用了更多的像素(16 个),并且包含了三次函数的计算,所以它的运算速度比双线性插值算法慢。
6.4 中值滤波器
非线性滤波器在图像处理中也起着很重要的作用。本节将介绍的中值滤波器就是其中的一种。
因为中值滤波器对消除椒盐噪声非常有用(这里用只有盐的噪声),所以我们将使用2.2 节创建的图像,如下所示。
调用中值滤波器函数的方法与调用其他滤波器差不多:
cv::medianBlur(image, result, 5);
// 最后一个参数是滤波器尺寸
结果如下所示。
因为中值滤波器是非线性的,所以不能用核心矩阵表示,也不能进行卷积运算。但它也是通过操作一个像素的邻域,来确定输出的像素值的。正如其名,中值滤波器把当前像素和它的邻域组成一个集合,然后计算出这个集合的中间值,以此作为当前像素的值(集合中数值经过排序,中间位置的数值就是中间值)。当前像素被中间值代替。
中值滤波器还有利于保留边缘的尖锐度,但它会洗去均质区域中的纹理(例如背景中的树木)。
6.5 用定向滤波器检测边缘
本节将执行一种反向的变换,即放大图像中的高频成分,再用本节介绍的高通滤波器进行边缘检测。
我们将要使用的滤波器称为Sobel 滤波器。因为它只对垂直或水平方向的图像频率起作用,所以被认为是一种定向滤波器。水平方向滤波器的调用方法为:
cv::Sobel(image, // 输入
sobelX, // 输出
CV_8U, // 图像类型
1, 0, // 内核规格
3, // 正方形内核的尺寸
0.4, 128); // 比例和偏移量
水平方向Sobel 算子得到的结果如下所示。
垂直方向滤波的调用方法为:
cv::Sobel(image, // 输入
sobelY, // 输出
CV_8U, // 图像类型
0, 1, // 内核规格
3, // 正方形内核的尺寸
0.4, 128); // 比例和偏移量
垂直方向Sobel 算子得到的结果如下所示。
你可以组合这两个结果(垂直和水平方向),得到Sobel 滤波器的范数:
// 计算Sobel 滤波器的范数
cv::Sobel(image,sobelX,CV_16S,1,0);
cv::Sobel(image,sobelY,CV_16S,0,1);
cv::Mat sobel;
// 计算L1 范数
sobel= abs(sobelX)+abs(sobelY);
在convertTo 方法中使用可选的缩放参数可得到一幅图像,图像中的白色用0 表示,更黑的灰色阴影用大于0 的值表示。这幅图像可以很方便地显示Sobel 算子的范数,代码如下所示:
// 找到Sobel 最大值
double sobmin, sobmax;
cv::minMaxLoc(sobel,&sobmin,&sobmax);
// 转换成8 位图像
// sobelImage = -alpha*sobel + 255
cv::Mat sobelImage;
sobel.convertTo(sobelImage,CV_8U,-255./sobmax,255);
得到的结果如下所示。
cv::Sobel 函数使用Sobel 内核来计算图像的卷积。函数的完整说明如下所示:
cv::Sobel(image, // 输入
sobel, // 输出
image_depth, // 图像类型
xorder, yorder, // 内核规格
kernel_size, // 正方形内核的尺寸
alpha, beta); // 比例和偏移量
在图像处理领域,通常把绝对值之和作为范数进行计算。这称为L1 范数,它得到的结果与L2 范数比较接近,但计算速度快。本节将采用L1 范数:
// 计算L1 范数
sobel= abs(sobelX)+abs(sobelY);
在检测边缘时,通常只计算范数。但如果需要同时计算范数和方向,可以使用下面的OpenCV函数:
// 计算Sobel 算子,必须用浮点数类型
cv::Sobel(image,sobelX,CV_32F,1,0);
cv::Sobel(image,sobelY,CV_32F,0,1);
// 计算梯度的L2 范数和方向
cv::Mat norm, dir;
// 将笛卡儿坐标换算成极坐标,得到幅值和角度
cv::cartToPolar(sobelX,sobelY,norm,dir);
6.6 计算拉普拉斯算子
拉普拉斯算子也是一种基于图像导数运算的高通线性滤波器,它通过计算二阶导数来度量图像函数的曲率。
在OpenCV 中,可用cv::Laplacian 函数计算图像的拉普拉斯算子。
我们为这个算子创建一个简单的类,封装几个与拉普拉斯算子有关的运算。基本的属性和方法如下所示:
class LaplacianZC {
private:
// 拉普拉斯算子
cv::Mat laplace;
// 拉普拉斯内核的孔径大小
int aperture;
public:
LaplacianZC() : aperture(3) {}
// 设置内核的孔径大小
void setAperture(int a) {
aperture = a;
}
// 计算浮点数类型的拉普拉斯算子
cv::Mat computeLaplacian(const cv::Mat& image) {
// 计算拉普拉斯算子
cv::Laplacian(image, laplace, CV_32F, aperture);
return laplace;
}
拉普拉斯算子的计算在浮点数类型的图像上进行。与上节一样,要对结果做缩放处理才能使其正常显示。缩放基于拉普拉斯算子的最大绝对值,其中数值0 对应灰度级128。类中有一个方法可获得下面的图像表示:
// 获得拉普拉斯结果,存在8 位图像中
// 0 表示灰度级128
// 如果不指定缩放比例,那么最大值会放大到255
cv::Mat getLaplacianImage(double scale = -1.0) {
if (scale < 0) {
double lapmin, lapmax;
// 取得最小和最大拉普拉斯值
cv::minMaxLoc(laplace, &lapmin, &lapmax);
// 缩放拉普拉斯算子到127
scale = 127 / std::max(-lapmin, lapmax);
}
// 生成灰度图像
cv::Mat laplaceImage;
laplace.convertTo(laplaceImage, CV_8U, scale, 128);
return laplaceImage;
}
使用这个类,从7×7 内核计算拉普拉斯图像的方法为:
// 用LaplacianZC 类计算拉普拉斯算子
LaplacianZC laplacian;
laplacian.setAperture(7); // 7×7 的拉普拉斯算子
cv::Mat flap = laplacian.computeLaplacian(image);
cv::Mat laplace = laplacian.getLaplacianImage();
得到的图像如下所示。