第 6 章 图像滤波


本章包括以下内容:

  • 用低通滤波器进行图像滤波;
  • 用滤波器进行缩减像素采样;
  • 用中值滤波器进行图像滤波;
  • 用定向滤波器检测边缘;
  • 计算图像的拉普拉斯算子。
boldt.jpg


6.2 低通滤波器

本节将介绍几种非常基本的低通滤波器。这种滤波器的目的是减少图像变化的幅度。要做到这点,一个简单的方法是把每个像素的值替换成它周围像素的平均值。这样一来,强度的快速变化会被消除,取而代之的是更加平滑的过渡。

cv::blur 函数将每个像素的值替换成该像素邻域的平均值(邻域是矩形的),从而使图像更加平滑。这个低通滤波器的用法如下所示:

cv::blur(image,result, cv::Size(5,5)); // 滤波器尺寸

这是使用滤波器后得到的结果。

result.jpg

有时需要让邻域内较近的像素具有更高的重要度。因此可计算加权平均值,即较近的像素比较远的像素具有更大的权重。要得到加权平均值,可采用依据高斯函数(即“钟形曲线”函数)制定的加权策略。函数cv::GaussianBlur 应用了这种滤波器,调用方法如下所示:

    cv::GaussianBlur(image, result,
        cv::Size(5, 5), // 滤波器尺寸
        1.5); // 控制高斯曲线形状的参数

得到的结果如下所示。

result.jpg

观察本节产生的输出图像,可以发现低通滤波器的最终效果是使图像更加模糊或更加平滑。这不奇怪,因为低通滤波器减弱了高频成分,而高频成分正好对应了物体边缘处的快速视觉变化。

对于高斯滤波器,像素对应的权重与它到中心像素之间的距离成正比。一维高斯函数的公式为:

G(x)=Ae^{-x^2/2\sigma^2}

使用归一化系数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); // 图像尺寸缩小一半
result.jpg

上述函数使用了一个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 节创建的图像,如下所示。

result.jpg

调用中值滤波器函数的方法与调用其他滤波器差不多:

cv::medianBlur(image, result, 5);
// 最后一个参数是滤波器尺寸

结果如下所示。

result.jpg

因为中值滤波器是非线性的,所以不能用核心矩阵表示,也不能进行卷积运算。但它也是通过操作一个像素的邻域,来确定输出的像素值的。正如其名,中值滤波器把当前像素和它的邻域组成一个集合,然后计算出这个集合的中间值,以此作为当前像素的值(集合中数值经过排序,中间位置的数值就是中间值)。当前像素被中间值代替。

中值滤波器还有利于保留边缘的尖锐度,但它会洗去均质区域中的纹理(例如背景中的树木)。


6.5 用定向滤波器检测边缘

本节将执行一种反向的变换,即放大图像中的高频成分,再用本节介绍的高通滤波器进行边缘检测。

我们将要使用的滤波器称为Sobel 滤波器。因为它只对垂直或水平方向的图像频率起作用,所以被认为是一种定向滤波器。水平方向滤波器的调用方法为:

cv::Sobel(image, // 输入
    sobelX, // 输出
    CV_8U, // 图像类型
    1, 0, // 内核规格
    3, // 正方形内核的尺寸
    0.4, 128); // 比例和偏移量

水平方向Sobel 算子得到的结果如下所示。

result.jpg

垂直方向滤波的调用方法为:

cv::Sobel(image, // 输入
    sobelY, // 输出
    CV_8U, // 图像类型
    0, 1, // 内核规格
    3, // 正方形内核的尺寸
    0.4, 128); // 比例和偏移量

垂直方向Sobel 算子得到的结果如下所示。

result.jpg

你可以组合这两个结果(垂直和水平方向),得到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);

得到的结果如下所示。

result.jpg

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();

得到的图像如下所示。

result.jpg
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容