目录
预备知识
滤波、核和卷积
边界外推和边界处理
阈值化操作
Otsu算法
自适应阈值
平滑
简单模糊和方框型滤波器
中值滤波器
高斯滤波器
双边滤波器
导数和梯度
索贝尔导数
Scharr滤波器
拉普拉斯变换
图像形态学
膨胀和腐蚀
通用形态学函数
开操作和闭操作
形态学梯度
顶帽和黑帽
自定义核
用任意线性滤波器做卷积
用cv::filter2D()进行卷积
通过cv::sepFilter2D使用可分核
生成卷积核
预备知识
滤波、核和卷积
滤波器指的是一种由一幅图像 I(x,y)根据像素点x,y附近的区域计算得到一幅新图像 I'(x,y)的算法。其中,模板规定了滤波器的形状以及这个区域内像素的值的组成规律,也称“滤波器”或“核”。在下面的介绍中多采用的是线性核,即 I'(x,y)的像素的值由 I(x,y)及其周围的像素的值的加权相加得来的。可由以下方程表示:
(A)5×5盒状核 | (B)规范化的5×5盒状核 | (C)3×3的Sobel核 | (D)5×5规范化高斯核 |
注:“锚点”均用粗体表示
边界外推和边界处理
自定义边框
在处理图像时,只要告诉调用的函数添加虚拟像素的规则,库函数就会自动创建虚拟像素。cv::copyMakeBorder()就是一个为图像创建边框的函数。
cv::copyMakeBorder(InputArray src, InputArray dst, int top, int bottom, int left, int right, int borderType, const cv::Scalar& value=cv::Scalar())
作用:通过指定两幅图像,同时指明填充方法,该函数就会将第一幅图填补后的结果保存在第二幅图像中。其中,src是原图像,dst是填充后的图像,top、bottom、left、right分别是四个方向上的尺寸,borderType是像素填充的方式,value是常量填充时的值。
取值 | 效果 |
---|---|
cv::BORDER_CONSTANT | 为每个边框像素赋予一个相同的值。 |
cv::BORDER_WRAP | 类似于平铺扩充 |
cv::BORDER_REPLICATE | 复制边缘的像素扩充 |
cv::BORDER_REFLECT | 通过镜像复制扩充 |
cv::BORDER_REFLECT_101 | 通过镜像复制扩充,边界像素除外 |
cv::BORDER_DEFAULT | cv::BORDER_REFLECT_101 |
自定义外推
int cv::borderInterpolate(int p, int len, int borderType)
作用:计算一个维度上的外推,p为原图上一个坐标,len是p指维度上的大小,borderType是边界类型。
例子:混合的边界条件下计算一个特定像素的值,在一维中使用BORDER_REFLECT_101,在二维中使用BORDER_WRAP:
float val = img.at<float>(cv::borderInterpolate(100, img.rows, cv::BORDER_REFLECT_101),cv::borderInterpolate(-5, img.cols, cv::BORDER_WRAP)
)
阈值化操作
阈值化操作的原理是对于数组中每个值,根据其高于或低于某个阈值做出相对应的处理,OpenCV中提供了实现这种功能的方法cv::threshold()。
double cv::threshold(InputArray src, cv::OutputArray dst, double thresh, double maxValue, int thresholdType)
阈值类型 | 操作 |
---|---|
cv::THRESH_BINARY | |
cv::THRESH_BINARY_INV | |
cv::THRESH_TRUNC | |
cv::THRESH_TOZERO | |
cv::THRESH_TOZERO_INV |
例一:将一幅图像的三个通道相加并将像素值限制在100以内
#include "stdafx.h"
#include <opencv2/opencv.hpp>
#include <iostream>using namespace std;void sum_rgb(const cv::Mat& src, cv::Mat& dst) {// 分通道vector<cv::Mat> planes;cv::split(src, planes);cv::Mat b = planes[0];cv::Mat g = planes[1];cv::Mat r = planes[2];cv::Mat s;// 加权融合,防止越界cv::addWeighted(r, 1. / 3, g, 1. / 3, 0.0, s);cv::addWeighted(s, 1., r, 1. / 3, 0.0, s);// 阈值化操作cv::threshold(s, dst, 100, 100, cv::THRESH_TRUNC);
}void help() {cout << "Call: ./ch10_ex10_1 faceScene.jpg" << endl;cout << "Show use of alpha blending (addWeighted) and threshold" << endl;
}int main()
{help();cv::Mat src = cv::imread("D:\\personal-data\\wallpapers\\test.png");cv::Mat dst;if (src.empty()){cout << "can not load the image" << endl;return -1;}sum_rgb(src, dst);cv::imshow("img", dst);cv::waitKey(0);cv::destroyAllWindows();return 0;
}
运行结果:
例二:对于浮点型图像进行三个通道相加并将像素值限制在100以内
#include "stdafx.h"
#include <opencv2/opencv.hpp>
#include <iostream>using namespace std;void sum_rgb(const cv::Mat& src, cv::Mat& dst) {// 分通道vector<cv::Mat> planes;cv::split(src, planes);cv::Mat b = planes[0];cv::Mat g = planes[1];cv::Mat r = planes[2];// 全0初始化s矩阵cv::Mat s = cv::Mat::zeros(b.size(), CV_32F);// accumulate可将8位整型的图像累加到一幅浮点型的图像中cv::accumulate(b, s);cv::accumulate(g, s);cv::accumulate(r, s);// 阈值化操作cv::threshold(s, dst, 100, 100, cv::THRESH_TRUNC);s.convertTo(dst, b.type());
}void help() {cout << "Call: ./ch10_ex10_1 faceScene.jpg" << endl;cout << "Show use of alpha blending (addWeighted) and threshold" << endl;
}int main()
{help();cv::Mat src = cv::imread("D:\\personal-data\\wallpapers\\test.png");cv::Mat dst;if (src.empty()){cout << "can not load the image" << endl;return -1;}sum_rgb(src, dst);cv::imshow("img", dst);cv::waitKey(0);cv::destroyAllWindows();return 0;
}
运行结果:
Otsu算法
函数cv::threshold()也可以自动决定最优的阈值,只需将参数thresh传递值cv::THRESH_OTSU即可。
简而言之,Otsu算法就是遍历所有可能的阈值,然后对每个阈值结果的两类像素(低于阈值和高于阈值两类像素)计算方差,然后计算的值,取其最小的阈值。
式中,和是根据两类像素的数量计算而来的权重。由于要遍历所有可能的阈值,所以这并不是一个相对高效的过程。
自适应阈值
自适应阈值方法中阈值在整个过程中自动产生变化,这由OpenCV中的cv::adaptiveThreshold()实现。
void cv::adaptiveThreshold(InputArray src, OutputArray dst, double maxValue, int adaptiveMethod, int threshType, int blockSize, double C)
该方法是逐个像素地计算自适应阈值T(x, y),具体计算过程是:计算每个像素位置周围的blockSize×blockSize区域的加权平均值,然后减去常数C。求均值时所用权重和adaptiveMethod有关,若是cv::ADAPTIVE_THRESH_MEAN_C,则权重相等,若是cv::ADAPTIVE_THRESH_GAUSSIAN_C,则权重由高斯方差得到。
注:该方法只适应与单通道8位或浮点型图像
#include "stdafx.h"
#include <opencv2/opencv.hpp>
#include <iostream>using namespace std;int main()
{// 设置参数double fixed_threshold = 15;int threshold_type = 1 ? cv::THRESH_BINARY : cv::THRESH_BINARY_INV;int adaptive_method = 1 ? cv::ADAPTIVE_THRESH_MEAN_C : cv::ADAPTIVE_THRESH_GAUSSIAN_C;int block_size = 71;double offset = 15;// 以灰度图形式加载图片cv::Mat Igray = cv::imread("D:\\personal-data\\wallpapers\\test.png", cv::IMREAD_GRAYSCALE);// 判断图像是否加载成功if (Igray.empty()) { cout << "Can not load " << "D:\\personal-data\\wallpapers\\test.png" << endl; return -1; }// 声明输出矩阵cv::Mat It, Iat;// 阈值化操作cv::threshold(Igray,It,fixed_threshold,255,threshold_type);// 自适应阈值cv::adaptiveThreshold(Igray,Iat,255,adaptive_method,threshold_type,block_size,offset);// 展示结果图像cv::imshow("Raw", Igray);cv::imshow("Threshold", It);cv::imshow("Adaptive Threshold", Iat);cv::waitKey(0);cv::destroyAllWindows();return 0;
}
运行结果:
当threshold_type=1且adaptive_method=1
原图 | 阈值化操作 | 自适应阈值化 |
当threshold_type=0且adaptive_method=0
原图 | 阈值化操作 | 自适应阈值化 |
平滑
平滑也称“模糊”,是一种简单而又常用的图像处理操作。平滑图像的目的有很多,但通常都是为了减少噪声和伪影。在降低图像分辨率的时候,平滑也十分重要,可以防止图片出现锯齿状。
简单模糊和方框型滤波器
void cv::blur(InputArray src,OutputArray dst,Size ksize,Point anchor=Point(-1,-1),int borderType=BORDER_DEFAULT)
作用:实现简单的滤波,目标图像中的每个值都是原图像中相应位置一个窗口(核)中像素的平均值,窗口的尺寸由ksize声明;anchaor指定计算时核与源图像的对齐方式,默认情况下anchor为cv::Point(-1, -1),表示核相对于滤波器居中。
void cv::boxFilter(InputArray src, OutputArray dst, cv::Size ksize, cv::Point anchor=cv::Point(-1,-1), bool normalize=true; int borderType=cv::BORDER_DEFAULT)
作用:方框滤波器是一种矩形的并且滤波器中所有值全部相等的滤波器。通常,所有的为1或者1/A,其中A是滤波器的面积。后一种滤波器称为“归一化方框型滤波器”,下面所示的是一个5×5的模糊滤波器,也称“归一化方框型滤波器”。
通过上述介绍,可以发现cv::boxFilter()是一种一般化的形式,而cv::blur()是一种特殊化的形式。但前者可以以非归一化形式调用,并且输出图像深度可以控制,但后者智能以归一化形式调用,且输出图像深度必须和原图像保持一致。
中值滤波器
中值滤波器(Median Filter)将每个像素替换为围绕这个像素的矩形领域内的中值或“中值”像素(相对于平均像素)。通过均值滤波器对噪声图像,尤其是有较大孤立的异常值非常敏感,少量具有较大偏差的点也会严重影响到均值滤波器。中值滤波器可以采用取其中间点的方式来消除异常值。
void cv::medianBlur(InputArray src, OutputArray dst, Size ksize)
高斯滤波器
关于高斯滤波器,在之前的文章中已做了详细介绍,可以参考OpenCV高斯滤波GaussianBlur
双边滤波器
双边滤波器是一种比较大的图像分析算子,也就是边缘保持平滑。高斯平滑的模糊过程是减缓像素在空间上的变化,因此与邻近的关系紧密,而随机噪声在像素间的变化幅度又会非常的大(即噪声不是空间相关的)。基于这种前提高斯平滑很好地减弱了噪声并且保留了小信号,但是却破坏了边缘信息,边缘也模糊了。
和高斯平滑类似,双边滤波对每个像素及其领域内的像素进行了加权平均。其权重由两部分组成,第一部分同高斯平滑,第二部分也是高斯权重,但是它不是基于空间距离而是色彩强度差计算而来的,在多通道(色彩)图像上强度差由各分量的加权累加代替。可将其当做高斯平滑,指示相似程度更高的像素权值更高,边缘更加明显,对比度更高。
cv::bilateralFilter(InputArray src, OutputArray dst, int d, double sigmaColor, double sigmaSpace, int borderType= cv::BORDER_DEFAULT)
参数:d是像素邻近的最大距离,处理视频时一般不大于5,非实时应用时可放大到9;sigmaColor是色彩空间滤波器的sigma值,该值越大,则色彩强度越大,不连续性越强;sigmaSpace是坐标空间滤波器的sigma值。
导数和梯度
卷积中最重要也是最基本的部分就是(近似)计算导数。
索贝尔导数
一般来说,用来表示微分的最常用的算子是索贝尔(Sobel)导数算子,可以实现任意阶导数和混合偏导数。
void sv::Sobel(InputArray src, OutputArray dst, int ddepth, int xorder, int yorder, cv::Size ksize=3, double scale=1, double delta=0, int borderType=cv::BORDER_DEFAULT)
参数:ddepth指明目标图像的深度或类型;xorder和yorder是求导顺序,其取值可为0,、1或2,其中0代表在该方向不求导,kszie是一个奇数,表示滤波器核的大小, 目前最大到31;阈值和偏移量在结果存入dst前进行调用,公式如下:
Sobel算子的好处是可以将核定义为各种大小,并且可以快速迭代式地构造这些核。大的核可以更好地近似导数,因为可以消除噪声影响。其缺点是如果导数在空间上变化剧烈,核太大会使结果发生偏差,并且核比较小时准确度不高。
实际上,由于Sobel算子定义在离散空间上,所以它并不是真正的导数,而是一个多项式,即在x方向上进行Sobel运算表示的并不是二阶导数,而是对抛物线函数的局部拟合。
Scharr滤波器
为了将图像内的信息联系起来,可能需要测量一幅图像:在处理过程中,通过在目标附近组织一幅梯度直方图来收集其形状信息,这些直方图是许多形状分类器训练和使用的基础。因此,梯度角的误差会降低分类器识别的效果。
对于3×3的Sobel滤波器,梯度角距离水平或垂直方向越远,误差越明显。在OpenCV中,调用cv::Sobel()时设置ksize为cv::SCHARR,即可消除3×3这样小但是快的Sobel导数滤波器所带来的误差。Scharr滤波器核Sobel滤波器同样很快,但是前者精度更高。因此选择3×3的滤波器时,应当使用Scharr滤波器。
或
拉普拉斯变换
OpenCV中的函数Laplacian实现了对拉普拉斯算子的离散近似:
void sv::Laplacian(InputArray src, OutputArray dst, int ddepth, cv::Size ksize=3, double scale=1, double delta=0, int borderType=cv::BORDER_DEFAULT)
只要ksize不为1,Laplacian算子的实现就是直接计算Sobel算子响应之和。当ksize=1时的卷积核如下所示:
Laplacian算子可应用于各种场景处理,一种常见的应用就是匹配“斑点”。Laplacian算子就是图像在x和y轴方向的导数之和,这意味着一个被较大值包围的点或小斑点(比ksize小)处的值将会变得很大。相反,被较小值包围的点或小斑点处的值将在负方向上变得很大。
Laplacian算子同样可以用于边缘检测,函数一阶导数在原函数变化大的地方,值会相应变大,同样在图像边缘处也同样变化,所有导数在这些地方将变得很大。因此可以在二阶导数为0的地方搜寻这么一个极大值,原图像中的边缘通过Laplacian算子运算后会变成0。对于有些不是边缘也变成0的问题,可以通过滤掉Sobel一阶导数中较大值的点解决。
图像形态学
OpenCV提供了一种高效且易用的图像形态学变换接口。其中有很多形态学方法,但基本上所有的形态学操作都基于两种原始操作——膨胀与腐蚀。
膨胀和腐蚀
膨胀和腐蚀是最基本的形态学变换,可用于消除噪声、元素分割和连接等。基于这两种操作,可以实现更复杂的形态学操作,用来定位强度峰值或孔洞、另一种形式的图像梯度等。
膨胀是一种卷积操作,它将目标像素的值替换为卷积核覆盖区域的局部最大值,扩张了明亮区域,填充凹面。此卷积核是一个非线性核,是一个四边形或圆形的实心核,其锚点在中心。与膨胀对应,腐蚀是与之相反的操作,腐蚀操作计算的是核覆盖范围内的局部最小值缩减了明亮区,消除凸起。
原图 | 膨胀 | 腐蚀 |
void cv::erode(InputArray src, OutputArray dst, InputArray element, cv::Point anchor=cv::Point(-1, -1), int iterations=1 int borderType=cv::BORDER_CONSTANT, const cv::Scalar& borderValue = cv::morphologyDefaultBorderValue())
void cv::dilate(InputArray src, OutputArray dst, InputArray element, cv::Point anchor=cv::Point(-1, -1), int iterations=1 int borderType=cv::BORDER_CONSTANT, const cv::Scalar& borderValue = cv::morphologyDefaultBorderValue())
以上两个函数分别是膨胀和腐蚀对应的函数,第三个参数是核,可以传递一个未初始化的cv::Mat,会使用默认的锚点在中心的3×3的核。
腐蚀操作通常用于消除图中斑点一样的噪声,原理是斑点经过腐蚀后会消失,而大的可见区域不会受影响。膨胀操作通常用于发生连通分支。
通用形态学函数
当处理的对象是二值图像时,像素只能是开(>0)或关(=0)的图像掩膜时,基本的腐蚀和膨胀操作就够用了。需要对灰度图或者彩色图进行处理时,一些其他操作就非常有用了,这些操作可以通过cv::morphologyEx()实现。
void cv::morphologyEx(InputArray src, OutputArray dst, int op, InputArray element, cv::Point anchor=cv::Point(-1, -1), int iterations=1, int borderType=cv::BORDER_CONSTANT, const cv::Scalar& borderValue = cv::morphologyDefaultBorderValue())
操作值 | 形态学操作 | 是否需要临时图像 |
---|---|---|
cv::MORPH_OPEN | 开操作 | 否 |
cv::MORPH_CLOSE | 闭操作 | 否 |
cv::MORPH_GRADIENT | 形态学梯度 | 总是需要 |
cv::MORPH_TOPHAT | 顶帽操作 | 就地调用需要(src = dst) |
cv::MORPH_BLACKHAT | 地貌操作 | 就地调用需要(src = dst) |
开操作和闭操作
开操作先将图像进行腐蚀,然后对腐蚀的结果进行膨胀。开操作常用语对二值图像中的区域进行计算。
闭操作想将图像进行膨胀,然后对膨胀的结果进行腐蚀。闭操作用于复杂连通分支算法中减少无用或噪声驱动的片段。
对于连通分支,通常先进行腐蚀或闭操作消除噪声,然后通过开操作连接相互靠近的大型区域。
对于一幅非布尔型图像进行形态学操作时,闭操作最明显的效果是消除值小于邻域内的点的孤立异常,而开操作消除的是大于邻域内点的孤立异常值。
形态学梯度
梯度操作的结果(扩张亮域)减腐蚀操作的结果(缩减亮域)产生了原图像中的目标边缘。对于灰度图像,其结果就是计算明暗变换的趋势。形态学梯度通常用于显示明亮区域的边界,然后便可以将他们看作目标或者目标的部分。用扩张的图像减去了收缩的图像便得到完整的边界。与计算梯度不同,它并不会关注某个物体的周围。
顶帽和黑帽
顶帽用于显示与其邻近相比更亮的部分;黑帽用于显示与其邻近相比更暗的部分。
#include "stdafx.h"
#include <opencv2/opencv.hpp>int main()
{cv::namedWindow("image", cv::WINDOW_NORMAL);cv::namedWindow("erosion", cv::WINDOW_NORMAL);cv::namedWindow("dilation", cv::WINDOW_NORMAL);cv::namedWindow("opening", cv::WINDOW_NORMAL);cv::namedWindow("closing", cv::WINDOW_NORMAL);cv::namedWindow("gradient", cv::WINDOW_NORMAL);cv::namedWindow("topHat", cv::WINDOW_NORMAL);cv::namedWindow("blackHat", cv::WINDOW_NORMAL);cv::Mat img = cv::imread("D:\\personal-data\\wallpapers\\test.png");cv::Mat erosion, dilation, opening, closing, gradient, topHat, blackHat;cv::erode(img, erosion, cv::Mat());cv::dilate(img, dilation, cv::Mat());cv::morphologyEx(img, opening, cv::MORPH_OPEN, cv::Mat());cv::morphologyEx(img, closing, cv::MORPH_CLOSE, cv::Mat());cv::morphologyEx(img, gradient, cv::MORPH_GRADIENT, cv::Mat());cv::morphologyEx(img, topHat, cv::MORPH_TOPHAT, cv::Mat());cv::morphologyEx(img, blackHat, cv::MORPH_BLACKHAT, cv::Mat());cv::imshow("image", img);cv::imshow("erosion", erosion);cv::imshow("dilation", dilation);cv::imshow("opening", opening);cv::imshow("closing", closing);cv::imshow("gradient", gradient);cv::imshow("topHat", topHat);cv::imshow("blackHat", blackHat);cv::waitKey(0);cv::destroyAllWindows();return 0;
}
自定义核
在形态学上,核常常称为“构造元素”,OpenCV提供了创建自定义形态学核的函数cv::getStructuringElement()。
cv::Mat cv::getStructuringElement(int shape, cv::Size ksize, cv::Point anchor=cv::Point(-1, -1))
形状值 | 元素 | 描述 |
---|---|---|
cv::MOEPH_RECT | 矩形 | |
cv::MOEPH_ELLIPSE | 椭圆形 | 以ksize.width和ksize.height为两个半径做椭圆 |
cv::MOEPH_CROSS | 交叉 | ,当 i == anchor.y或 j == anchor.x |
用任意线性滤波器做卷积
上述两个核中,左边的核是可分的,右边的是不可分的。一个可分核可以理解成两个一维核,在卷积时先调用x内核,然后再调用y内核。两个矩阵进行卷积所产生的消耗可以用两个矩阵的面积之积近似。如此一来,用n×n的核对面积为A的图像进行卷积所需要的时间是,但如果分解为n×1和1×n的两个核,那么代价就是。由此可见,分解卷积核可以提高卷积计算的效率。
用cv::filter2D()进行卷积
void cv::filter2D(InputArray src, OutputArray dst, int ddepth, InputArray kernel, cv::Point anchor=cv::Point(-1, -1), double delta=0, int borderType=cv::BORDER_DEFAULT)
注:如果定义了锚点的位置,那么核的大小可以是偶数,否则必须是奇数。
通过cv::sepFilter2D使用可分核
void cv::sepFilter2D(InputArray src, OutputArray dst, int ddepth, InputArray rowKernel, InputArray columnKernel, cv::Point anchor=cv::Point(-1, -1), double delta=0, int borderType=cv::BORDER_DEFAULT)
注:两个核的大小应当是n1×1和1×n2,n1和n2不一定相等。
生成卷积核
void cv::getDerivKernel(OutputArray kx, OutputArray ky, int dx, int dy, int ksize, bool normalize=true, int ktype=CV_32F)
作用:生成可分解核,如Sobel和Scharr核。dx和dy是求导顺序;ksize是核的大小,可以为1、3、5、7或cv::SCHARR;normalize指示是否核元素规范化,如果是浮点型图像,设为true,反之设为false;ktype表示滤波器的类型,可以使CV_32F和CV_64F。
cv::Mat cv::getGaussianKernel(int ksize, double sigma, int ktype=CV_32F)
作用:生成高斯核。
α在滤波器需要规范化的时候才起作用。sigma可以为-1,这样将自动计算,其中