imgpro 组件是 Image 和 Process 这两个单词的缩写组合,即图像处理模块,这个模块包含了如下内容:
平滑处理(smoothing)也称模糊处理(bluring),是一种简单且使用频率很高的图像处理方法。最常见的用途是用来减少图像上的噪点或者失真。
图像滤波,指在尽量保留图像细节特征的条件下对目标图像的噪声进行抑制,是图像预处理中不可缺少的操作,其处理效果的好坏将直接影响到后续图像处理的分析和有效性和可靠性。
消除图像中的噪声成分叫做图像的平滑化或滤波操作。信号或图像的能量大部分集中在幅度谱的低频和中频段,而在较高频段,有用的信息经常被噪声淹没。因此一个能降低高频成分幅度的滤波器就能减弱噪声的影响。
图像滤波的目的有两个:一个是抽出对象的特征作为图像识别的特征模式,另一个是为适应图像处理的要求,消除图像数字化时所混入的噪声。
而对滤波处理的要求也有两条:一是不能损坏图像的轮廓及边缘等重要信息,二是使图像清晰视觉效果好。
平滑滤波是低频增强的空间滤波技术。它的目的有两类:一类是模糊,另一类是消除噪声。
空间域的平滑滤波一般采用简单平均法进行,就是求邻近像元点平均亮度值。邻域的大小与平滑的效果直接相关,邻域越大平滑效果越好,但邻域过大,平滑也会使边缘信息损失的越大,从而使输出的图像变得模糊,因此需合理选择邻域的大小。
关于滤波器,一种形象的比喻是:可以把滤波器想象成一个包含加权系数的窗口,当使用这个滤波器平滑处理图像时,就可以把这个窗口放到图像之上,透过这个窗口来看我们得到的图像。
滤波器的种类有很多,在 OpenCV 中,提供了如下 5 种常用的图像平滑处理操作方法。
● 方框滤波------BoxBlur 函数
● 均值滤波(邻域平均滤波)------ Blur 函数
● 高斯滤波------GaussianBlur 函数
● 中值滤波------medianBlur 函数
● 双边滤波------bilateralFilter 函数
其中,线性滤波的有方框滤波、均值滤波和高斯滤波,非线性滤波的有中值滤波和双边滤波
线性滤波器:常用于剔除输入信号中不想要的频率或者从许多频率中选择一个想要的频率。
几种常见的线性滤波器如下:
● 低通滤波器:允许低频率通过。
● 高通滤波器:允许高频率通过。
● 带通滤波器:允许一定范围频率通过。
● 带阻滤波器:阻止一定范围频率通过并且允许其他频率通过。
● 全通滤波器:允许所有频率通过,仅仅改变相位关系。
● 陷波滤波器:阻止一个狭窄频率范围通过,是一种特殊的带阻滤波器。
以高斯滤波为例,滤波可分为低通滤波和高通滤波两种:高斯滤波是指用高斯函数作为滤波函数的滤波操作,至于是不是模糊,要看是高斯低通还是高斯高通,低通就是模糊,高通就是锐化。
● 高斯滤波是指用高斯函数作为滤波函数的滤波操作。
● 高斯模糊就是高斯低通滤波。
邻域算子(局部算子)是利用给定像素周围的像素值的决定此像素的最终输出值的一种算子。而线性邻域滤波就是一种常用的邻域算子,像素的输出值取决于输入像素的加权和。
邻域算子除了用于局部色调调整以外,还可以用于图像滤波,以实现图像的平滑和锐化,图像边缘增强或者图像噪声的去除。
线性滤波处理的输出像素值 g(i,j) 是输入像素值 f(i + k , j + I)h(k, I) 的加权和,如下:
其中的 h(k, l),我们称其为 “核”,是滤波器的加权系数,即滤波器的 ""滤波系数 。
上面的式子可以简单写作:
其中 f 表示输入像素值,h 表示加权系数 “核”,g 表示输出像素值。
方框滤波被封装在 boxblur 函数中,作用是使用方框滤波器来模糊一张图片,从 src 输入,从 dst 输出。
函数原型如下:
void boxFilter(InputArray src,
OutputArray dst,
int ddepth,
Size ksize,
Point anchor = Point(-1, -1),
boolnormalize = true,
int borderType = BORDER_DEFAULT
)
● 第一个参数:src,输入图像。该函数对通道是独立处理的,且可以处理任意通道的图片,但待处理的图片深度应该为 CV_8U、CV_16U、CV_16S、CV_32F 以及 CV_64F 之一。
● 第二个参数:dst,目标图像。
● 第三个参数:ddepth,输出图像的深度,-1 代表使用原图深度,即 src.depth()。
● 第四个参数:Size 类型的 ksize,内核的大小。一般采用 Size(w,h) 来表示内核的大小,Size(5,5) 就表示 5x5 的核大小。
● 第五个参数:Point 类型的 anchor,表示锚点(即被平滑的那个点)。它有默认值 Point(-1,-1) 。如果这个点做标是负值的话,就表示取核的中心为锚点,所以默认值 Point(-1,-1) 表示这个锚点在核的中心。
● 第六个参数,bool 类型的 normalize ,默认值为 true,一个标识符,表示内核是否被其区域归一化了。
● 第七个参数:int 类型的 borderType,用于推断图像外部像素的某种边界模式。有默认值 BORDER_DEFAULT,一般不用管它。
BoxFilter() 函数方框滤波所用的核表示如下:
其中:
上式中 f 表示原图,h 表示核,g 表示目标图,当 normalize = true 时,方框滤波就变成了均值滤波,也就是说,均值滤波是方框滤波归一化后的特殊情况。其中,归一化就是把要处理的量都放缩到一个范围内,比如(0,1),以便统一处理和直观量化。而非归一化的方框滤波用于计算每个像素邻域内的积分特性,比如密集光流算法中用到的图像倒数的协方差矩阵。
如果我们要在可变的窗口中计算像素总和,可以使用 inegral() 函数。
下面是一个方框滤波函数的使用示例:
#include
#include
#include
using namespace std;
using namespace cv;
int main(int argc, char** argv)
{
//载入原图
Mat image = imread("8.jpg");
//创建窗口
namedWindow("均值滤波原图");
namedWindow("均值滤波效果图");
//显示窗口
imshow("均值滤波原图", image);
//进行滤波操作
Mat out;
boxFilter(image, out, -1, Size(5, 5));
//显示效果图
imshow("均值滤波效果图", out);
waitKey(0);
//system("pause");
return 0;
}
运行效果图(内核大小Size(5,5))
运行效果图(内核大小Size(10,10))
均值滤波,是最简单的一种滤波操作,输出图像的每一个像素是核窗口内输入图像对应像素的平均值(所有像素加权系数相等),说白了就是归一化之后的方框滤波。
均值滤波是典型的线性滤波算法,主要方法为邻域平均法,即用一片图像区域的各个像素的均值来代替原图像中的各个像素值。
均值滤波本身存在着固有的缺陷,即它不能很好地保护图像细节,在图像去噪的同时也破坏了图像的细节部分,从而使图像变得模糊,不能很好地去除噪声点。
blur 函数的作用是:对输入的图像 src 进行均值滤波后用 dst 输出。
其内核是这样的:
blur 函数原型如下:
void blur(InputArray src,
OutputArrat dst,
Size ksize,
Point anchor = Point(-1, -1),
int borderType = BORDER_DEFAULT
)
● 第一个参数:src,输入图像。该函数对通道是独立处理的,且可以处理任意通道的图片,但待处理的图片深度应该为 CV_8U、CV_16U、CV_16S、CV_32F 以及 CV_64F 之一。
● 第二个参数:dst,目标图像。
● 第三个参数:Size 类型的 ksize,内核的大小。一般采用 Size(w,h) 来表示内核的大小,Size(5,5) 就表示 5x5 的核大小。
● 第四个参数:Point 类型的 anchor,表示锚点(即被平滑的那个点)。它有默认值 Point(-1,-1) 。如果这个点做标是负值的话,就表示取核的中心为锚点,所以默认值 Point(-1,-1) 表示这个锚点在核的中心。
● 第五个参数:int 类型的 borderType,用于推断图像外部像素的某种边界模式。有默认值 BORDER_DEFAULT,一般不用管它。
调用代码示例:
#include
#include
#include
using namespace std;
using namespace cv;
int main(int argc, char** argv)
{
//载入原图
Mat image = imread("8.jpg");
//创建窗口
namedWindow("均值滤波原图");
namedWindow("均值滤波效果图");
//显示窗口
imshow("均值滤波原图", image);
//进行滤波操作
Mat out;
blur(image, out, Size(5, 5));
//显示效果图
imshow("均值滤波效果图", out);
waitKey(0);
//system("pause");
return 0;
}
运行效果图(内核大小Size(5,5))
高斯滤波是一种线性平滑的滤波,可以消除高斯噪声,广泛运用于图像处理的减噪过程。通俗的将,高斯滤波就是对整幅图像进行加权平均后得到。高斯滤波的具体操作是:用一个模板(或称卷积、掩膜)扫描图像中的每一个像素,用模板确定的邻域内像素的加权平均灰度值去替代模板中心像素点的值。
图像与圆形方框模糊做卷积会生成更加精确的焦外成像效果。由于高斯函数的傅里叶变换是另外一个高斯函数,所以高斯模糊对于图像来说就是一个低通滤波操作。
高斯滤波器是一类根据高斯函数的形状来选择权值的线性平均滤波器。高斯平滑滤波器对于抑制服从正态分布的噪声非常有效。一维零均值高斯函数如下:
其中,高斯分布参数 Sigma 决定了高斯函数的宽度。对于图像处理来说,常用二维零均值离散高斯函数作为平滑滤波器。
二维高斯函数如下:
高斯滤波函数 GaussianBlur 函数的作用是用高斯滤波器来模糊一张图片,对输入的图像 src 进行高斯滤波后用 dst 输出。它将源图像和指定的高斯核函数做卷积运算,并且支持就地过滤。
void GaussianBlur(InputArray src,
OutputArray dst,
Size ksize,
double sigmaX,
double sigmaY = 0,
int borderType = BORDER_DEFAULT
)
● 第一个参数:src ,输入图像,它可以是单独的任意通道的图片,但需要注意的是,其图片深度应该为 CV_8U、CV_16U、CV_16S、CV_32F 以及 CV_64F 之一。
● 第二个参数:dst,即目标图像。
● 第三个参数:Size 类型的 ksize ,高斯内核的大小,其中 ksize.width 和 ksize.height 可以不同,但它们必须为正数和奇数,或者是零,这都由 sigma 计算而来。
● 第四个参数:double 类型的 sigmaX,表示高斯核函数在 X 方向的标准偏差。
● 第五个参数:double 类型的 sigmaY ,表示高斯核函数在 Y 方向的标准偏差。若 sigmaY 为零,就将它设为 sigmaX,如果 sigmaX 和 sigmaY 都是零,那么就由 ksize.width 和 ksize.height 计算出来。
● 第五个参数:int 类型的 borderType,用于推断图像外部像素的某种边界模式。有默认值 BORDER_DEFAULT,一般不用管它。
调用代码示例:
#include
#include
#include
using namespace std;
using namespace cv;
int main(int argc, char** argv)
{
//载入原图
Mat image = imread("8.jpg");
//创建窗口
namedWindow("高斯滤波原图");
namedWindow("高斯滤波效果图");
//显示窗口
imshow("高斯滤波原图", image);
//进行滤波操作
Mat out;
GaussianBlur(image, out, Size(5, 5), 0, 0);
//显示效果图
imshow("高斯滤波效果图", out);
waitKey(0);
//system("pause");
return 0;
}
运行效果图(内核大小Size(5,5))
#include
#include
#include
using namespace std;
using namespace cv;
Mat g_srcImage, g_dstImage1, g_dstImage2, g_dstImage3;
int g_nBoxFilterValue = 3; //方框滤波参数值
int g_nMeanBlurValue = 3; //均值滤波参数值
int g_nGaussianBlurValue = 3; //高斯滤波参数值
//方框滤波操作的回调函数
void on_BoxFilter(int, void *)
{
//方框滤波操作
boxFilter(g_srcImage, g_dstImage1, -1, Size(g_nBoxFilterValue + 1, g_nBoxFilterValue + 1));
//显示窗口
imshow("方框滤波", g_dstImage1);
}
//均值滤波操作的回调函数
void on_MeanFilter(int, void *)
{
//均值滤波操作
blur(g_srcImage, g_dstImage2, Size(g_nMeanBlurValue + 1, g_nMeanBlurValue + 1), Point(-1, -1));
//显示窗口
imshow("均值滤波", g_dstImage2);
}
//高斯滤波操作的回调函数
void on_GaussianBlur(int, void *)
{
//高斯滤波操作
GaussianBlur(g_srcImage, g_dstImage3, Size(g_nGaussianBlurValue * 2 + 1, g_nGaussianBlurValue * 2 + 1), 0, 0);
//显示窗口
imshow("高斯滤波", g_dstImage3);
}
int main(int argc, char** argv)
{
//改变 console 字体颜色
system("color 5E");
//载入原图
g_srcImage = imread("6.jpg", 1);
if (!g_srcImage.data)
{
printf("图像读取失败!");
return 0;
}
//复制原图到三个目标图中
g_dstImage1 = g_srcImage.clone();
g_dstImage2 = g_srcImage.clone();
g_dstImage3 = g_srcImage.clone();
//显示原图
namedWindow("原始图", 1);
imshow("原始图", g_srcImage);
//-------------方框滤波-------------
//创建窗口
namedWindow("方框滤波", 1);
//创建轨迹条
createTrackbar("内核值:", "方框滤波", &g_nBoxFilterValue, 40, on_BoxFilter);
on_BoxFilter(g_nBoxFilterValue, 0);
//-------------均值滤波-------------
//创建窗口
namedWindow("均值滤波", 1);
//创建轨迹条
createTrackbar("内核值:", "均值滤波", &g_nMeanBlurValue, 40, on_MeanFilter);
on_MeanFilter(g_nMeanBlurValue, 0);
//-------------高斯滤波-------------
//创建窗口
namedWindow("高斯滤波", 1);
//创建轨迹条
createTrackbar("内核值:", "高斯滤波", &g_nGaussianBlurValue, 40, on_GaussianBlur);
on_GaussianBlur(g_nGaussianBlurValue, 0);
waitKey(0);
//system("pause");
return 0;
}
上面所讲的滤波器都是线性的,即两个信号之和的响应和它们各自响应之和相等。换句话说,每个像素的输出值是一些输入像素的加权和。
在很多情况下,使用邻域像素的非线性滤波会得到更好的效果。比如在噪声是散粒噪声而不是高斯噪声,即图像偶尔会出现很大的值的时候,用高斯滤波器对图像进行模糊的话,噪声像素是不会被去除的,只是转换为更为柔和但仍然可见的散粒。
中值滤波(Median filter)是一种典型的非线性滤波技术,基本思想是用像素点邻域灰度值的中值来代替该像素点的灰度值,该方法在去除脉冲噪声、椒盐噪声的同时又能保留图像的边缘细节。
中值滤波是基于排序统计理论的一种能有效抑制噪声的非线性信号处理技术,其基本原理是把数字图像或数字序列中一点的值用该点的一个邻域中各点值的中值代替。让周围的像素值接近真实值,从而消除孤立的噪声点。这对于斑点噪声和椒盐噪声来说尤其有用,因为它不依赖于邻域内那些与典型值差别很大的值。中值滤波器在处理连续图像函数时与线性滤波器的工作方式类似,但滤波过程不再是加权运算。
中值滤波在一定的条件下可以克服常见的线性滤波器,如最小均方滤波、方框滤波器、均值滤波器等带来的图像细节模糊,而且对滤除脉冲干扰及图像扫描噪声非常有效,也常用于保护边缘信息。保存边缘的特性使它在不希望出现边缘模糊的场合也很有用,是非常经典的平滑噪声处理方法。
● 中值滤波与均值滤波的比较
优势:在均值滤波器中,由于噪声成分被放入平均计算中,所以输出受到了噪声的影响。但是在中值滤波器中,由于噪声成分很难选上,所以几乎不会影响到输出。因此同样用 3x3 区域进行处理,中值滤波消除噪声的能力更胜一筹。中值滤波无论是在消除噪声还是保存边缘方面都是一个不错的方法。
劣势:中值滤波花费的时间是均值滤波的 5 倍以上。
中值滤波对于一些细节(特别是细、尖顶等)多的图像不太适合。
双边滤波(Bilateral filter)是一种非线性的滤波方法,是结合图像的空间邻近度和像素值相似度的一种折中处理,同时考虑空域信息和灰度相识性,达到保边去噪的目的,具有简单、非迭代、局部的特点。
双边滤波的好处是可以做边缘保存。双边滤波器比高斯滤波多了一个高斯方差 sigma - d,它是基于空间分布的高斯滤波函数,所以在边缘附近,离得较远的像素不会对边缘上的像素值影响太多,这样就保证了边缘附近像素值的保存。但是,由于保存了太多的高频信息,对于彩色图像里的高频噪声,双边滤波器不能够干净地滤掉,只能对于低频信息进行较好的滤波。
在双边滤波器中,输出的像素值依赖于临域像素值的加权组合,公式如下:
而加权系数 w(i,j,k,l) 取决于定义域核和值域核的乘积。
其中定义域核表示如下:
值域核如下:
定义域滤波和值域滤波如图:
两者相乘后,就会产生依赖与数据的双边滤波权重函数:
medianBlur 函数使用中值滤波器来平滑(模糊)处理一张图片,从 src 输入,结果从 dst 输出。对于多通道图片,它对每一个通道都单独进行处理,并且支持就地操作。
void medianBlur(InputArray src, OutputArray dst, int ksize)
● 第一个参数:InputArray 类型的 src,函数的输入参数,填 1、3 或者 4 通道的 Mat 类型的图像。当 ksize 为 3 或 5 的时候,图像深度需 为 CV_8U、CV_16U、CV_32F 其中之一,而对于较大孔尺寸的图片时,它只能是 CV_8U。
● 第二个参数:OutputArray 类型的 dst,即目标图像,需与源图一样的尺寸和类型,可用 Mat::Clone() ,以源码为模板,来得到目标图。
● 第三个参数:int 类型的 ksize,孔径的线性尺寸,注意这个参数必须是大于 1 的奇数,比如:3、5、7、9······
调用示例如下:
#include
#include
#include
using namespace std;
using namespace cv;
int main(int argc, char** argv)
{
//改变 console 字体颜色
system("color 5E");
Mat image = imread("2.jpg");
imshow("中值滤波原图", image);
Mat out;
medianBlur(image, out, 7);
imshow("中值滤波效果图", out);
waitKey(0);
//system("pause");
return 0;
}
medianBlur 函数使用中值滤波器来模糊处理一张图片,从 src 输入,结果从 dst 输出。
void bilateralFilter(InputArray src,
OutputArray dst,
int d,
double sigmaColor,
double sigmaSpace,
int borderType = BORDER_DEFAULT
)
● 第一个参数:InputArray 类型的 src,函数的输入参数,需为 8 位或者浮点型单通道、三通道的图像。
● 第二个参数:OutputArray 类型的 dst,即目标图像,需与源图一样的尺寸和类型,可用 Mat::Clone() ,以源码为模板,来得到目标图。
● 第三个参数:int 类型的 d,表示在过滤过程中每个像素邻域的直径。如果这个值被设为非正数,那么 OpenCV 会从第五个参数 sigmaSpace 来计算出它。
● 第四个参数:double 类型的 sigmaColor,颜色空间中滤波器的 sigma 值。它的数值越大,就表明该像素邻域内有越宽广的颜色会被混合到一起,产生较大的半径相等的颜色区域。
● 第五个参数:double 类型的 sigmaSpace,坐标空间中滤波器的 sigma 值,坐标空间的标注方差。它的数值越大,意味着越远的像素会互相影响,从而使更大区域中足够相似的颜色获取相同的颜色。当 d > 0 时, d 指定了邻域大小且与 sigmaSpace 无关,否则, d 正比于 sigmaSpace。
● 第六个参数:int 类型的 borderType,用于推断图像外部像素的某种边界模式,有默认值 BORDER_DEFAULT。
调用示例如下:
#include
#include
#include
using namespace std;
using namespace cv;
int main(int argc, char** argv)
{
//改变 console 字体颜色
system("color 5E");
Mat image = imread("2.jpg");
imshow("双边滤波原图", image);
Mat out;
bilateralFilter(image, out, 25, 25 * 2, 25 / 2);
imshow("双边滤波效果图", out);
waitKey(0);
//system("pause");
return 0;
}
#include
#include
#include
using namespace std;
using namespace cv;
Mat g_srcImage, g_dstImage1, g_dstImage2, g_dstImage3,g_dstImage4, g_dstImage5;
int g_nBoxFilterValue = 6; //方框滤波参数值
int g_nMeanBlurValue = 10; //均值滤波参数值
int g_nGaussianBlurValue = 6; //高斯滤波参数值
int g_nMedianBlurValue = 10; //中值滤波参数值
int g_nBilateralFilterValue = 10; //双边滤波参数值
//方框滤波操作的回调函数
void on_BoxFilter(int, void *)
{
//方框滤波操作
boxFilter(g_srcImage, g_dstImage1, -1,
Size(g_nBoxFilterValue + 1, g_nBoxFilterValue + 1));
//显示窗口
imshow("方框滤波", g_dstImage1);
}
//均值滤波操作的回调函数
void on_MeanFilter(int, void *)
{
//均值滤波操作
blur(g_srcImage, g_dstImage2,
Size(g_nMeanBlurValue + 1, g_nMeanBlurValue + 1), Point(-1, -1));
//显示窗口
imshow("均值滤波", g_dstImage2);
}
//高斯滤波操作的回调函数
void on_GaussianBlur(int, void *)
{
//高斯滤波操作
GaussianBlur(g_srcImage, g_dstImage3,
Size(g_nGaussianBlurValue * 2 + 1, g_nGaussianBlurValue * 2 + 1), 0, 0);
//显示窗口
imshow("高斯滤波", g_dstImage3);
}
//中值滤波的回调函数
void on_MedianBlur(int, void *)
{
medianBlur(g_srcImage, g_dstImage4, g_nMeanBlurValue * 2 + 1);
imshow("中值滤波", g_dstImage4);
}
//双边滤波操作的回调函数
void on_BilateralFilter(int, void *)
{
bilateralFilter(g_srcImage, g_dstImage5, g_nBilateralFilterValue,
g_nBilateralFilterValue * 2, g_nBilateralFilterValue / 2);
imshow("双边滤波", g_dstImage5);
}
int main(int argc, char** argv)
{
//改变 console 字体颜色
system("color 5E");
//载入原图
g_srcImage = imread("1.jpg", 1);
if (!g_srcImage.data)
{
printf("图像读取失败!");
return 0;
}
//复制原图到五个目标图中
g_dstImage1 = g_srcImage.clone();
g_dstImage2 = g_srcImage.clone();
g_dstImage3 = g_srcImage.clone();
g_dstImage4 = g_srcImage.clone();
g_dstImage5 = g_srcImage.clone();
//显示原图
namedWindow("原始图", 1);
imshow("原始图", g_srcImage);
//-------------方框滤波-------------
//创建窗口
namedWindow("方框滤波", 1);
//创建轨迹条
createTrackbar("内核值:", "方框滤波", &g_nBoxFilterValue, 50, on_BoxFilter);
on_BoxFilter(g_nBoxFilterValue, 0);
//-------------均值滤波-------------
//创建窗口
namedWindow("均值滤波", 1);
//创建轨迹条
createTrackbar("内核值:", "均值滤波", &g_nMeanBlurValue, 50, on_MeanFilter);
on_MeanFilter(g_nMeanBlurValue, 0);
//-------------高斯滤波-------------
//创建窗口
namedWindow("高斯滤波", 1);
//创建轨迹条
createTrackbar("内核值:", "高斯滤波", &g_nGaussianBlurValue, 50, on_GaussianBlur);
on_GaussianBlur(g_nGaussianBlurValue, 0);
//-------------中值滤波-------------
//创建窗口
namedWindow("中值滤波", 1);
//创建轨迹条
createTrackbar("内核值:", "中值滤波", &g_nMedianBlurValue, 50, on_MedianBlur);
on_MedianBlur(g_nMedianBlurValue, 0);
//-------------双边滤波-------------
//创建窗口
namedWindow("双边滤波", 1);
//创建轨迹条
createTrackbar("内核值:", "双边滤波", &g_nBilateralFilterValue, 50, on_BilateralFilter);
on_BilateralFilter(g_nBilateralFilterValue, 0);
waitKey(0);
//system("pause");
return 0;
}
图像处理中的形态学,通常指的是数学形态学。
数学形态学,是一门建立在格论和拓扑学基础之上的图像分析学科,是数学形态学图像处理的基本理论。其基本的运算包括:二值腐蚀和膨胀、二值开闭运算、骨架抽取、极限腐蚀、击中击不中变换、形态学梯度、Top-hat 变换、颗粒分析、流域变换、灰值腐蚀和膨胀、灰值开闭运算、灰值形态学梯度等。
最基本的形态学操作有两种:膨胀和腐蚀。
膨胀与腐蚀能实现多种多样的功能,主要如下:
● 消除噪声。
● 分割出独立的图像元素,在图像中连接邻近的元素。
● 寻找图像中明显的极大值与极小值区域。
● 求出图像的梯度。
下图是用于对比膨胀与腐蚀运算的“浅墨”字样的毛笔字原图
需要注意的是,膨胀和腐蚀操作是对白色部分(高亮部分)而言的,而不是黑色部分。膨胀是图像中的高亮部分进行膨胀,类似于“领域扩张”,效果图拥有比原图更大的高亮区域;腐蚀是原图中的高亮部分被腐蚀,类似于“领域被蚕食”,效果图拥有比原图更小的高亮区域。
膨胀(dilate)是求局部最大值的操作。从数学角度来说,膨胀或腐蚀操作就是将图像(或图像的一部分区域,称为 A)与核(称为 B)进行卷积。
核可以是任何形状和大小,它拥有一个单独定义出来的点,我们称其为锚点(anchorpoint)。多数情况下,核是一个小的,中间带有参考点和实心正方形或者圆盘。其实,可以把核视为模板或者掩码。
核 B 与图形做卷积,即计算核 B 覆盖的区域的像素点的最大值,并把这个最大值赋给参考点指定的像素。这样就会使图像中的高亮区域逐渐增长,这就是膨胀操作的初衷。
膨胀的数学表达式如下:
下图是毛笔字和图片膨胀的效果示例:
膨胀和腐蚀(erode)是一对相反的操作,所以腐蚀就是求局部最小值的操作。
腐蚀操作示例图如下:
腐蚀的数学表达式如下:
毛笔字和照片的腐蚀操作效果图:
函数原型如下:
void dilate(
InputArray src,
OutputArray dst,
InputArray kernel,
Point anchor = Point(-1, -1),
int iterations = 1,
int borderType = BORDER_CONSTANT,
const Scalar& borderValue = morphologyDefaultBorderValue()
);
● 第一个参数:InputArray 类型的 src,输入图像,即源图像,通道数可以是任意的,但深度应为 CV_8U、CV_16U、CV_16S、CV_32F、CV_64F 其中之一。
● 第二个参数:OutputArray 类型的 dst,即目标图像,需要和源图像一样的尺寸和类型。
● 第三个参数:InputArray 类型的 kernel,膨胀操作的核。当为 NULL 时,表示的是使用参考点位于中心 3x3 的核。
我们一般使用 getStructuringElement 配合这个参数的使用。getStructuringElement 会返回指定形状和尺寸的结构元素(内核矩阵)。其中,getStructuringElement 函数的第一个参数表示内核的形状,有如下三种形状可选:
●矩形:MORPH_RECT;
●交叉形:MORPH_CROSS;
●椭圆形:MORPH_ELLIPSE;
而 getStructuringElement 函数的第二和第三个参数分别是内核的尺寸和锚点的位置。
一般在调用 erode 以及 dilate 函数之前,先定义一个 Mat 类型的变量来获得getStructuringElement 函数的返回值。对于锚点位置,有默认值 Point(-1, -1),表示锚点位于中心。此外,需要注意,十字形的 element 形状唯一依赖于锚点的位置,而在其他情况下,锚点只是影响了形态学运算结果的偏移。
getStructuringElement 函数相关调用示例如下:
int g_nStructElementSize = 3; //结构元素(内核矩阵)的尺寸
//获取自定义核
Mat element = getStructuringElement(MORPH_RECT,
Size(2 * g_nStructElementSize + 1, 2 * g_nStructElementSize + 1),
Point( g_nStructElementSize , g_nStructElementSize ));
erode 函数与 dilate 函数的第三个参数就是上述代码中的 element 变量。
● 第四个参数:Point 类型的 anchor,锚的位置,其有默认值(-1, -1),表示锚位于中心。
● 第五个参数:int 型的 iterations,迭代使用 dilate() 函数的次数,默认值为 1。
● 第六个参数:int 类型的 borderType ,用于推断图像外部像素的某种边界模式,有默认值 BORDER_DEFAULT。
● 第七个参数:const Scalar& 类型的 borderValue,当边界为常数时的边界值,有默认
值 morphologyDefaultBorderValue() ,一般不用管它。
使用 dilate 函数,一般只需填前三个参数,后面四个参数有默认值,而且往往会结合 getStructuringElement 一起使用。
下面给出一个示例:
#include
#include
#include
using namespace std;
using namespace cv;
int main(int argc, char** argv)
{
Mat image = imread("8.jpg");
imshow("原图", image);
//获取自定义核
Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));
Mat out;
dilate(image, out, element);
imshow("膨胀效果图", out);
waitKey(0);
//system("pause");
return 0;
}
erode 函数使用像素邻域内的局部极小值运算符来腐蚀一张图片,从 src 输入,由 dst 输出,支持就地操作。
函数原型如下:
void erode(
InputArray src,
OutputArray dst,
InputArray kernel,
Point anchor = Point(-1, -1),
int iterations = 1,
int borderType = BORDER_CONSTANT,
const Scalar& borderValue = morphologyDefaultBorderValue()
);
● 第一个参数:InputArray 类型的 src,输入图像,即源图像,通道数可以是任意的,但深度应为 CV_8U、CV_16U、CV_16S、CV_32F、CV_64F 其中之一。
● 第二个参数:OutputArray 类型的 dst,即目标图像,需要和源图像一样的尺寸和类型。
● 第三个参数:InputArray 类型的 kernel,膨胀操作的核。当为 NULL 时,表示的是使用参考点位于中心 3x3 的核,一般使用函数 getStructuringElement 函数配合这个参数使用。
● 第四个参数:Point 类型的 anchor,锚的位置,其有默认值(-1, -1),表示锚位于中心。
● 第五个参数:int 型的 iterations,迭代使用 erode() 函数的次数,默认值为 1。
● 第六个参数:int 类型的 borderType ,用于推断图像外部像素的某种边界模式,有默认值 BORDER_DEFAULT。
● 第七个参数:const Scalar& 类型的 borderValue,当边界为常数时的边界值,有默认
值 morphologyDefaultBorderValue() ,一般不用管它。
使用 dilate 函数,一般只需填前三个参数,后面四个参数有默认值,而且往往会结合 getStructuringElement 一起使用。
下面给出一个示例:
#include
#include
#include
using namespace std;
using namespace cv;
int main(int argc, char** argv)
{
Mat image = imread("8.jpg");
imshow("原图", image);
//获取自定义核
Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));
Mat out;
erode(image, out, element);
imshow("腐蚀效果图", out);
waitKey(0);
//system("pause");
return 0;
}
#include
#include
#include
using namespace std;
using namespace cv;
Mat g_srcImage, g_dstImage;
int g_nTrackbarNumber = 0; //0 表示腐蚀,1 表示膨胀
int g_nStructElementSize = 3; //结构元素(内核矩阵)的尺寸
//进行自定义的腐蚀和膨胀操作
void process()
{
//获取自定义核
Mat element = getStructuringElement(MORPH_RECT,
Size(2 * g_nStructElementSize + 1, 2 * g_nStructElementSize + 1),
Point(g_nStructElementSize, g_nStructElementSize));
//进行腐蚀或膨胀操作
if (g_nTrackbarNumber == 0)
{
erode(g_srcImage, g_dstImage, element);
}
else {
dilate(g_srcImage, g_dstImage, element);
}
imshow("效果图", g_dstImage);
}
//腐蚀和膨胀操作直接切换开关的回调函数
void on_TrackbarNumChange(int, void *)
{
//腐蚀和膨胀之间的效果已经切换,回调函数体内只需调用一次 Process 函数,使改变后的效果显示出来
process();
}
//腐蚀和膨胀操作内核改变时的回调函数
void on_ElementSizeChange(int, void *)
{
//内核尺寸已经改变,回调函数体内只需调用一次 process 函数,使改变后的效果立即显示出来
process();
}
int main(int argc, char** argv)
{
g_srcImage = imread("7.jpg");
if (!g_srcImage.data)
{
printf("图像读取失败!");
return 0;
}
imshow("原始图", g_srcImage);
//进行初次腐蚀操作并显示效果图
namedWindow("效果图");
//获取自定义核
Mat element = getStructuringElement(MORPH_RECT,
Size(2 * g_nStructElementSize + 1, 2 * g_nStructElementSize + 1),
Point(g_nStructElementSize, g_nStructElementSize));
erode(g_srcImage, g_dstImage, element);
imshow("效果图", g_dstImage);
//创建轨迹条
createTrackbar("腐蚀/膨胀", "效果图", &g_nTrackbarNumber, 1, on_TrackbarNumChange);
createTrackbar("内核尺寸", "效果图", &g_nStructElementSize, 21, on_ElementSizeChange);
waitKey(0);
//system("pause");
return 0;
}
腐蚀和膨胀是两种最基本的形态学操作,形态学的高级形态,往往是建立在腐蚀和膨胀这两个基本操作之上的。
开运算(Opening Operation),其实就是先腐蚀后膨胀的过程。其数学表达式如下:
dst = open(src, element) = dilate(erode(src, element))
开运算可以用来消除小物体,在纤细点处分离物体,并且在平滑较大物体的边界的同时不明显改变其面积。
开运算原图与效果图:
毛笔字原图:
毛笔字效果图:
照片原图与效果图:
与开运算相反,先膨胀后腐蚀的过程称之为闭运算(Closing Operation),其数学表达式如下:
dst = close(src, element) = erode(dilate(src, element))
闭运算能够排除小型黑洞(黑色区域)。
形态学梯度(Morphological Gradient)是膨胀图与腐蚀图之差,数学表达式如下:
dst = morph - grad(src, element) = dilate(src, element) - erode(src, element)
对二值图进行这一操作可以将团块(blob)的边缘突出出来。我们可以用形态学梯度来保留物体的边缘轮廓。
顶帽运算(Top Hat)又常常被译为 “礼帽” 运算,是原图像与开运算的结果图之差,数学表达式如下:
dst = tophat(src ,element) = src - open(src, element)
因为开运算带来的结果是放大了裂缝或者局部低亮度的区域。因此,从原图中减去开运算后的图,得到的效果图突出了比原图轮廓周围的区域更明亮的区域,且这一操作与选择的核的大小相关。
顶帽运算常常用于分离比邻近点亮一些的斑块。在一幅图像具有大幅的背景,而微小物品比较有规律的情况下,可以使用顶帽运算进行背景提取。
黑帽(Black Hat)运算是闭运算的结果图与原图像之差。数学表达式为:
dst = blackhat(src, element) = close(src, element) - src
黑帽运算后的效果图突出了比原图轮廓周围的区域更暗的区域,且这一操作和选择的核的大小相关。
所以,黑帽运算用来分离比邻近点暗一些的斑块,效果图有着非常完美的轮廓。
morphologyEx() 函数利用基本的膨胀和腐蚀技术,来执行更加高级的形态学变换。
函数原型为:
void morphologyEx(
InputArray src,
OutputArray dst,
int op,
InputArray kernel,
Point anchor = Point(-1, -1),
int iterations = 1,
int borderType = BORDER_CONSTANT,
const Scalar& borderValue = porphologyDefaultBorderValue()
);
● 第一个参数:InputArray 类型的 src,输入图像,即源图像,通道数可以是任意的,但深度应为 CV_8U、CV_16U、CV_16S、CV_32F、CV_64F 其中之一。
● 第二个参数:OutputArray 类型的 dst,即目标图像,需要和源图像一样的尺寸和类型。
● 第三个参数:int 类型的 op ,表示形态学运算的类型,可以是下表中任一标识符。
● 第四个参数:InputArray 类型的 kernel,形态学运算的内核。当为 NULL 时,表示的是使用参考点位于中心 3x3 的核。
我们一般使用 getStructuringElement 配合这个参数的使用。getStructuringElement 会返回指定形状和尺寸的结构元素(内核矩阵)。其中,getStructuringElement 函数的第一个参数表示内核的形状,有如下三种形状可选:
●矩形:MORPH_RECT;
●交叉形:MORPH_CROSS;
●椭圆形:MORPH_ELLIPSE;
而 getStructuringElement 函数的第二和第三个参数分别是内核的尺寸和锚点的位置。
一般在调用 erode 以及 dilate 函数之前,先定义一个 Mat 类型的变量来获得getStructuringElement 函数的返回值。对于锚点位置,有默认值 Point(-1, -1),表示锚点位于中心。此外,需要注意,十字形的 element 形状唯一依赖于锚点的位置,而在其他情况下,锚点只是影响了形态学运算结果的偏移。
getStructuringElement 函数相关调用示例如下:
int g_nStructElementSize = 3; //结构元素(内核矩阵)的尺寸
//获取自定义核
Mat element = getStructuringElement(MORPH_RECT,
Size(2 * g_nStructElementSize + 1, 2 * g_nStructElementSize + 1),
Point( g_nStructElementSize , g_nStructElementSize ));
erode 函数与 dilate 与 morphologyEx 函数的 kernel 参数就是上述代码中的 element 变量。
● 第五个参数:Point 类型的 anchor,锚的位置,其有默认值(-1, -1),表示锚位于中心。
● 第六个参数:int 型的 iterations,迭代使用 dilate() 函数的次数,默认值为 1。
● 第七个参数:int 类型的 borderType ,用于推断图像外部像素的某种边界模式,有默认值 BORDER_DEFAULT。
● 第八个参数:const Scalar& 类型的 borderValue,当边界为常数时的边界值,有默认
值 morphologyDefaultBorderValue() ,一般不用管它。
使用 dilate 函数,一般只需填前三个参数,后面四个参数有默认值,而且往往会结合 getStructuringElement 一起使用。
#include
#include
#include
using namespace std;
using namespace cv;
Mat g_srcImage, g_dstImage;
int g_nElementShape = MORPH_RECT; //元素结构的形状
//变量接收的 TrackBar 位置参数
int g_nOpenCloseNum = 0;
int g_nMaxIterationNum = 10;
int g_nErodeDilateNum = 0;
int g_nTopBlackHatNum = 0;
//开运算/闭运算窗口的回调函数
void on_OpenClose(int ,void *)
{
//偏移量的定义,结合滑动条来说,当滑动条当前的值大于初始值时偏移量为正,小于为负
int offset = g_nOpenCloseNum - g_nMaxIterationNum; //偏移量
int Absolute_offset = offset > 0 ? offset : -offset; //偏移量绝对值
//自定义核
Mat element = getStructuringElement(g_nElementShape,
Size(Absolute_offset * 2 + 1, Absolute_offset * 2 + 1),
Point(Absolute_offset, Absolute_offset)
);
//进行偏移操作
if (offset < 0)
{
morphologyEx(g_srcImage, g_dstImage, MORPH_OPEN, element);
}
else {
morphologyEx(g_srcImage, g_dstImage, MORPH_CLOSE, element);
}
imshow("开运算/闭运算", g_dstImage);
}
//顶帽/黑帽窗口的回调函数
void on_TopBlackHat(int , void *)
{
//偏移量的定义
int offset = g_nTopBlackHatNum - g_nMaxIterationNum; //偏移量
int Absolute_offset = offset > 0 ? offset : -offset; //偏移量绝对值
//自定义核
Mat element = getStructuringElement(g_nElementShape,
Size(Absolute_offset * 2 + 1, Absolute_offset * 2 + 1),
Point(Absolute_offset, Absolute_offset)
);
//进行偏移操作
if (offset < 0)
{
morphologyEx(g_srcImage, g_dstImage, MORPH_TOPHAT, element);
}
else {
morphologyEx(g_srcImage, g_dstImage, MORPH_BLACKHAT, element);
}
imshow("顶帽/黑帽", g_dstImage);
}
//腐蚀/膨胀窗口的回调函数
void on_ErodeDilate(int , void *)
{
//偏移量的定义
int offset = g_nErodeDilateNum - g_nMaxIterationNum; //偏移量
int Absolute_offset = offset > 0 ? offset : -offset; //偏移量绝对值
//自定义核
Mat element = getStructuringElement(g_nElementShape,
Size(Absolute_offset * 2 + 1, Absolute_offset * 2 + 1),
Point(Absolute_offset, Absolute_offset)
);
//进行偏移操作
if (offset < 0)
{
erode(g_srcImage, g_dstImage, element);
}
else {
dilate(g_srcImage, g_dstImage, element);
}
imshow("腐蚀/膨胀", g_dstImage);
}
int main(int argc, char** argv)
{
g_srcImage = imread("1.jpg");
if (!g_srcImage.data)
{
printf("图像读取失败!");
return 0;
}
imshow("原始图", g_srcImage);
//创建三个窗口
namedWindow("开运算/闭运算");
namedWindow("腐蚀/膨胀");
namedWindow("顶帽/黑帽");
//参数赋值
g_nOpenCloseNum = 10;
g_nErodeDilateNum = 10;
g_nTopBlackHatNum = 10;
//分别为三个窗口创建轨迹条
createTrackbar("迭代值", "腐蚀/膨胀", &g_nErodeDilateNum,
g_nMaxIterationNum * 2 + 1, on_ErodeDilate);
createTrackbar("迭代值", "开运算/闭运算", &g_nOpenCloseNum,
g_nMaxIterationNum * 2 + 1, on_OpenClose);
createTrackbar("迭代值", "顶帽/黑帽", &g_nTopBlackHatNum,
g_nMaxIterationNum * 2 + 1, on_TopBlackHat);
//执行回调函数
on_OpenClose(g_nOpenCloseNum, 0);
on_ErodeDilate(g_nErodeDilateNum, 0);
on_TopBlackHat(g_nTopBlackHatNum, 0);
waitKey(0);
//system("pause");
return 0;
}
漫水填充是一种用特定的颜色填充连通区域,通过设置可连通像素的上下限以及连通方式来达到不同的填充效果的方法。漫水填充经常被用来标记或分离图像的一部分,以便对其进行进一步的处理或分析,也可以用来从输入图像获取掩码区域,掩码会加速处理过程,或只处理掩码指定的像素点,操作的结果总是某个连续的区域。
漫水填充,简单来说,就是自动选中了和种子店相连的区域,接着讲该区域替换成指定的颜色。
floodFill 函数的作用是:用我们指定的颜色从种子点开始填充一个连接域,其连通性由像素值的接近程度来衡量。
有两个 C++ 重写版本的 floodFill,具体如下:
//第一个版本
int floodFill(InputOutputArray image,
Point seedPoint,
Scalar newVal,
Rect* rect = 0,
Scalar loDiff = Scalar(),
Scalar upDiff = Scalar(),
int flag = 4
)
//第二个版本
int floodFill(InputOutputArray image,
InputOutputArray mask,
Point seedPoint,
Scalar newVal,
Rect* rect = 0,
Scalar loDiff = Scalar(),
Scalar upDiff = Scalar(),
int flags = 4
)
● 第一个参数:InputOutputArray 类型的 image,输入/输出 1 通道或 3 通道,8 位或浮点图像,具体由之后的参数指定。
● 第二个参数:InputOutputArray 类型的 mask,这是第二个版本的 floodFill 函数独享的参数,表示掩膜。它应为单通道,8 位,长和宽上都比输入图像 image 大两个像素点的图像。需要注意的是,漫水填充不会填充掩膜 mask 的非零像素区域。例如,一个边缘检测算子的输出可以用来作为掩膜,以保证填充的区域不会重叠。另外需要注意的是,掩膜 mask 会比需填充的图像大,所以 mask 中与输入图像(x, y)像素点相对应的点的坐标为(x +1, y + 1)。
● 第三个参数:Point 类型的 seedPoint,漫水填充算法的起始点。
● 第四个参数:Scalar 类型的 newVal,像素点被染色的值,即在重绘区域像素的新值。
● 第五个参数:Rect* 类型的 rect ,有默认值 0,一个可选的参数,用于设置 floodFill 函数将要重绘区域的最小边界矩形区域。
● 第六个参数:Scalar 类型的 loDiff,有默认值 Scalar(),表示当前观察像素值与其部件邻域像素值或者待加入该部件的种子像素之间的亮度或颜色之负差(lower brightnes/color difference)的最大值。
● 第七个参数:Scalar 类型的 upDiff,有默认值 Scalar(),表示当前观察像素值与其部件邻域像素值或待加入该部件的种子像素之间的亮度或颜色之正差(lower brightness/color difference)的最大值。
● 第八个参数:int 类型的 flags,操作符标志,此参数包含三个部分:
1)低八位(0 ~ 7 位)用于控制算法的连通性,可取 4(4 为默认值)或者 8。如果设为 4,表示填充算法只考虑当前像素水平方向和垂直方向的相邻点。如果设为 8,除上述相邻点外,还会包含对角线方向的相邻点。
2)高八位部分(16 ~ 23 位),可以为 0 或者如下两种选项标识符的组合。
● FLOODFILL_FIXED_RANGE:如果设置为这个标识符,就会考虑当前像素与种子像素之间的差,否则就考虑当前像素与其相邻像素的差。也就是说,这个范围是浮动的。
● FLOODFILL_MASK_ONLY:如果设为这个标识符,函数不会去填充改变原始图像(也就是忽略第三个参数 newVal),而是去填充掩膜图像(mask)。这个标识符只对第二个版本的 floodFill 函数有效。
3)中间八位部分,用于指定填充掩码图像的值,但如果 flags 中间八位的值为 0,则掩码就会用 1 来填充。
所有 flags 可以用 or 操作符连接起来,即 “|”。例如,如果想用 8 邻域填充,并填充固定像素值的范围,填掩码而不是填充源图像,以及设填充值为 38,那么输入的参数如下:
flags = 8 | FLOODFILL_MASK_ONLY | FLOODFILL_FIXED_RANGE | (38 << 8)
下面是一个关于 floodFill 的简单调用范例:
#include
#include
#include
using namespace std;
using namespace cv;
int main(int argc, char** argv)
{
Mat src = imread("mogu.jpg");
imshow("原始图", src);
Rect ccomp;
floodFill(src, Point(50, 300), Scalar(155, 155, 55),
&ccomp, Scalar(20, 20, 20), Scalar(20, 20, 20));
imshow("效果图", src);
waitKey(0);
//system("pause");
return 0;
}
#include
#include
#include
using namespace std;
using namespace cv;
//-----------------------------------【全局变量声明部分】--------------------------------------
// 描述:全局变量声明
//-----------------------------------------------------------------------------------------------
Mat g_srcImage, g_dstImage, g_grayImage, g_maskImage;//定义原始图、目标图、灰度图、掩模图
int g_nFillMode = 1;//漫水填充的模式
int g_nLowDifference = 20, g_nUpDifference = 20;//负差最大值、正差最大值
int g_nConnectivity = 4;//表示floodFill函数标识符低八位的连通值
bool g_bIsColor = true;//是否为彩色图的标识符布尔值
bool g_bUseMask = false;//是否显示掩膜窗口的布尔值
int g_nNewMaskVal = 255;//新的重新绘制的像素值
//-----------------------------------【ShowHelpText( )函数】----------------------------------
// 描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
static void ShowHelpText()
{
printf("\n\n\t按键操作说明: \n\n"
"\t\t鼠标点击图中区域- 进行漫水填充操作\n"
"\t\t键盘按键【ESC】- 退出程序\n"
"\t\t键盘按键【1】- 切换彩色图/灰度图模式\n"
"\t\t键盘按键【2】- 显示/隐藏掩膜窗口\n"
"\t\t键盘按键【3】- 恢复原始图像\n"
"\t\t键盘按键【4】- 使用空范围的漫水填充\n"
"\t\t键盘按键【5】- 使用渐变、固定范围的漫水填充\n"
"\t\t键盘按键【6】- 使用渐变、浮动范围的漫水填充\n"
"\t\t键盘按键【7】- 操作标志符的低八位使用4位的连接模式\n"
"\t\t键盘按键【8】- 操作标志符的低八位使用8位的连接模式\n\n");
}
//-----------------------------------【onMouse( )函数】--------------------------------------
// 描述:鼠标消息onMouse回调函数
//---------------------------------------------------------------------------------------------
static void onMouse(int event, int x, int y, int, void*)
{
// 若鼠标左键没有按下,便返回
if (event != EVENT_LBUTTONDOWN)
return;
//-------------------【<1>调用floodFill函数之前的参数准备部分】---------------
Point seed = Point(x, y);
int LowDifference = g_nFillMode == 0 ? 0 : g_nLowDifference;//空范围的漫水填充,此值设为0,否则设为全局的g_nLowDifference
int UpDifference = g_nFillMode == 0 ? 0 : g_nUpDifference;//空范围的漫水填充,此值设为0,否则设为全局的g_nUpDifference
//标识符的0~7位为g_nConnectivity,8~15位为g_nNewMaskVal左移8位的值,16~23位为CV_FLOODFILL_FIXED_RANGE或者0。
int flags = g_nConnectivity + (g_nNewMaskVal << 8) + (g_nFillMode == 1 ? FLOODFILL_FIXED_RANGE : 0);
//随机生成bgr值
int b = (unsigned)theRNG() & 255;//随机返回一个0~255之间的值
int g = (unsigned)theRNG() & 255;//随机返回一个0~255之间的值
int r = (unsigned)theRNG() & 255;//随机返回一个0~255之间的值
Rect ccomp;//定义重绘区域的最小边界矩形区域
Scalar newVal = g_bIsColor ? Scalar(b, g, r) : Scalar(r*0.299 + g * 0.587 + b * 0.114);//在重绘区域像素的新值,若是彩色图模式,取Scalar(b, g, r);若是灰度图模式,取Scalar(r*0.299 + g*0.587 + b*0.114)
Mat dst = g_bIsColor ? g_dstImage : g_grayImage;//目标图的赋值
int area;
//--------------------【<2>正式调用floodFill函数】-----------------------------
if (g_bUseMask)
{
threshold(g_maskImage, g_maskImage, 1, 128, THRESH_BINARY);
area = floodFill(dst, g_maskImage, seed, newVal, &ccomp, Scalar(LowDifference, LowDifference, LowDifference),
Scalar(UpDifference, UpDifference, UpDifference), flags);
imshow("mask", g_maskImage);
}
else
{
area = floodFill(dst, seed, newVal, &ccomp, Scalar(LowDifference, LowDifference, LowDifference),
Scalar(UpDifference, UpDifference, UpDifference), flags);
}
imshow("效果图", dst);
cout << area << " 个像素被重绘\n";
}
//-----------------------------------【main( )函数】--------------------------------------------
// 描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main(int argc, char** argv)
{
//改变console字体颜色
system("color 2F");
//载入原图
g_srcImage = imread("1.jpg", 1);
if (!g_srcImage.data) { printf("读取图片image0错误~! \n"); return false; }
//显示帮助文字
ShowHelpText();
g_srcImage.copyTo(g_dstImage);//拷贝源图到目标图
cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);//转换三通道的image0到灰度图
g_maskImage.create(g_srcImage.rows + 2, g_srcImage.cols + 2, CV_8UC1);//利用image0的尺寸来初始化掩膜mask
namedWindow("效果图", WINDOW_AUTOSIZE);
//创建Trackbar
createTrackbar("负差最大值", "效果图", &g_nLowDifference, 255, 0);
createTrackbar("正差最大值", "效果图", &g_nUpDifference, 255, 0);
//鼠标回调函数
setMouseCallback("效果图", onMouse, 0);
//循环轮询按键
while (1)
{
//先显示效果图
imshow("效果图", g_bIsColor ? g_dstImage : g_grayImage);
//获取键盘按键
int c = waitKey(0);
//判断ESC是否按下,若按下便退出
if ((c & 255) == 27)
{
cout << "程序退出...........\n";
break;
}
//根据按键的不同,进行各种操作
switch ((char)c)
{
//如果键盘“1”被按下,效果图在在灰度图,彩色图之间互换
case '1':
if (g_bIsColor)//若原来为彩色,转为灰度图,并且将掩膜mask所有元素设置为0
{
cout << "键盘“1”被按下,切换彩色/灰度模式,当前操作为将【彩色模式】切换为【灰度模式】\n";
cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);
g_maskImage = Scalar::all(0); //将mask所有元素设置为0
g_bIsColor = false; //将标识符置为false,表示当前图像不为彩色,而是灰度
}
else//若原来为灰度图,便将原来的彩图image0再次拷贝给image,并且将掩膜mask所有元素设置为0
{
cout << "键盘“1”被按下,切换彩色/灰度模式,当前操作为将【彩色模式】切换为【灰度模式】\n";
g_srcImage.copyTo(g_dstImage);
g_maskImage = Scalar::all(0);
g_bIsColor = true;//将标识符置为true,表示当前图像模式为彩色
}
break;
//如果键盘按键“2”被按下,显示/隐藏掩膜窗口
case '2':
if (g_bUseMask)
{
destroyWindow("mask");
g_bUseMask = false;
}
else
{
namedWindow("mask", 0);
g_maskImage = Scalar::all(0);
imshow("mask", g_maskImage);
g_bUseMask = true;
}
break;
//如果键盘按键“3”被按下,恢复原始图像
case '3':
cout << "按键“3”被按下,恢复原始图像\n";
g_srcImage.copyTo(g_dstImage);
cvtColor(g_dstImage, g_grayImage, COLOR_BGR2GRAY);
g_maskImage = Scalar::all(0);
break;
//如果键盘按键“4”被按下,使用空范围的漫水填充
case '4':
cout << "按键“4”被按下,使用空范围的漫水填充\n";
g_nFillMode = 0;
break;
//如果键盘按键“5”被按下,使用渐变、固定范围的漫水填充
case '5':
cout << "按键“5”被按下,使用渐变、固定范围的漫水填充\n";
g_nFillMode = 1;
break;
//如果键盘按键“6”被按下,使用渐变、浮动范围的漫水填充
case '6':
cout << "按键“6”被按下,使用渐变、浮动范围的漫水填充\n";
g_nFillMode = 2;
break;
//如果键盘按键“7”被按下,操作标志符的低八位使用4位的连接模式
case '7':
cout << "按键“7”被按下,操作标志符的低八位使用4位的连接模式\n";
g_nConnectivity = 4;
break;
//如果键盘按键“8”被按下,操作标志符的低八位使用8位的连接模式
case '8':
cout << "按键“8”被按下,操作标志符的低八位使用8位的连接模式\n";
g_nConnectivity = 8;
break;
}
}
return 0;
}
在 OpenCV 中,操纵图像的尺寸有两种方法:
● resize 函数。
● pyrUp() 、pyrDown() 函数,即图像金字塔相关的两个函数,对图像进行向上采样和向下采样的操作。
图像金字塔是图像中多尺度表达的一种,最主要用于图像分割,是一种以多分辨率来解释图像的有效但概念简答的结构。
图像金字塔最初用于机器视觉和图像压缩,一幅图像的金字塔是一系列以金字塔形状排列的,分辨率逐步降低且来源于同一张原始图像的集合。其通过梯次向下采样获得,直到达到某个终止条件才停止采样。
金字塔的底部是待处理图像的高分辨率表示,而顶部是低分辨率的近视。
我们将一层一层的图像比喻成金字塔,层级越高,则图像越小,分辨率越低。
一般情况下有两类的金字塔经常出现:
● 高斯金字塔————用来向下采样,主要的图像金字塔。
● 拉普拉斯金字塔————用来从金字塔底层图像重建上层未采样图像,在数字图像处理中也即是预测残差,可以对图像进行最大程度的还原,配合高斯金字塔一起使用。
要从 第 i 层生成第 i + 1 层(我们将第 i + 1 层表示为 G(i + 1)),我们先要用高斯核对 Gi 进行卷积,然后删除所有偶数行和偶数列,新得到图像面积会变为源图像的四分之一。按上述过程对输入图像 G0 执行操作就可产生整个金字塔。
当图像向金字塔上层移动时,尺寸和分辨率会降低。OpenCV 中,从金字塔中上一级图像生成下一级图像的可以用 PryDown,而通过 PryUp 将现有的图像每个维度都放大两遍。
这里的向上采样和向下采样,是针对图像的尺寸而言的(和金字塔方向相反),向上就是图像尺寸加倍,向下就是图像尺寸减半。而金字塔向上其实是在缩小,这样刚好反过来了。
但需要注意的是,PryUp 和 PryDown 不是互逆的。图像首先在每个维度上扩大为原来的两倍,新增的行(偶数行)以 0 填充。然后给指定的滤波器进行卷积(实际上是一个每个维度都扩大为原来的两倍的过滤器)去估计 “丢失” 像素的近似值。
PryDown() 是一个会丢失信息的函数。为了恢复原来更高分辨率的图像,我们要获得由降采样操作丢失的信息,这些数据就和拉普拉斯金字塔有关系了。
高斯金字塔是通过高斯平滑和亚采样来获得一些下列采样图像,也就是说第 K 层高斯金字塔通过平滑、亚采样就可以获得 K + 1 层高斯图像。高斯金字塔包含了一系列低通滤波器,其截止频率从上一层到下一层以因子 2 逐渐增加,所以高斯金字塔可以跨越很大的频率范围。金字塔图像如图所示:
另外,每一层都按从下到上的次序编号,层级 G(i + 1)(表示为 G(i + 1) 尺寸小于第 i 层 Gi)。
1)对图像的向下取样
为了获取层级为 G(i + 1) 的金字塔图像,我们采样如下方法:
①:对图像 Gi 进行高斯内核卷积;
②:将所有偶数行和列去除。
得到的图像即为 G(i + 1) 图像。结果图像只有原图像的四分之一。通过对图像不停迭代上述步骤就会得到整个金字塔,同时,向下取样会逐渐丢失图像的信息。
以上就是对图像的向下取样操作,即缩小图像。
2)对图像的向上取样
①:将图像在每个方向扩大为原来的两倍,新增的行和列以 0 填充。
②:使用先前同样的内核(乘以 4)与放大后的图像卷积,获得 “新增像素” 的近似值。
得到的图像即为放大后的图像,但相比于原来的图像会比较模糊,因为在缩放的过程中已经丢失了一些信息。
下式为拉普拉斯金字塔第 i 层的数学定义:
式中的 Gi 表示第 i 层的图像,而 UP() 操作是将原图像中位置为 (x,y) 的像素映射到目标图像的 (2x + 1,2y + 1)位置,即在进行向上取样。式子中的符号表示卷积,g 为 5x5 的高斯内核。
也就是说,拉普拉斯金字塔是通过源图像减去缩小后的再放大的图像的一系列图像构成的。
整个拉普拉斯金字塔运算过程可以通过下图概括:
所以,我们可以将拉普拉斯金字塔理解为高斯金字塔的逆形式。
另外,关于图像金字塔的一个非常重要的应用就是图像分割。图像分割的话,要建立一个图像金字塔,然后对 Gi 和 G(i + 1) 的像素直接依照对应的关系,建立 “夫” 与 “子” 的关系。而快速初始分割可以先在金字塔高层的低分辨率图像上完成,然后逐层对分割加以优化。
resize() 函数将源图像精确地转换为指定尺寸的目标图像。如果源图像中设置了 ROI ,那么 resize() 函数就会对 ROI 区域进行调整图像尺寸的操作,来输出到目标图像中。若目标图像中设置了 ROI 区域,则 resize() 函数将会对源图像进行尺寸调整并填充到目标图像的 ROI 中。
它的图像原型为:
void resize(InputArray src,
OutputArray dst,
Size dsize,
double fx = 0,
double fy = 0,
int interpolation = INTER_LINEAR
)
● 第一个参数:InputArray 类型的 src ,源图像。
● 第二个参数:OutputArray 类型的 dst,输出图像,当其为非零时,有着 dsize(第三个参数)尺寸,或者由 src.size() 计算出来。
● 第三个参数:Size 类型的 dsize,输出图像的大小,如果它等于 0,由下式计算:
dsize = Size(round(fx * src.cols), round(fy * src.rows) )
其中,dsize、fx、fy 都不能为 0。
● 第四个参数:double 类型的 fx,沿水平轴的缩放系数,有默认值 0,且其等于 0 时,由下式进行计算:
(double)dsize.height/src.rows
● 第六个参数:int 类型的 interpolation ,用于指定插值的方式,默认值为 INTER_LINEAR(线性插值)。
可选的插值方式如下:
1)INTER_NEAREST——最近邻插值。
2)INTER_LINEAR——线性插值。
3)INTER_AREA——区域插值。(利用像素区域关系的重采样插值)
4)INTER_CUBIC——三次样条插值(超过 4x4 像素邻域内的双三次插值)
5)INTER_LANCZOS4——Lanczos 插值(超过 8x8 像素邻域的 Lanczos 插值)
若要缩小图像,一般情况下最好用 INTER_AREA 来插值,而若要放大图像,一般情况下最好用 INTER_LINEAR。
关于插值示例图片:
原图片:
当图像完成 6 次缩小放大操作后,两种不同插值方式得到的图片:
两种 resize 的调用范例:
1)方式一:
Mat dstImage = Mat::zeros(512, 512, CV_8UC3); //新建一张 512 x 512 尺寸的图片
Mat srcImage = imread("1.jpg");
//显示指定 dsize = dstImage.size() ,那么 fx 和 fy 会根据其计算出来,不用额外指定
resize(srcImage, dstImage, dstImage.size());
1)方式二:
Mat dstImage = Mat::zeros(512, 512, CV_8UC3); //新建一张 512 x 512 尺寸的图片
Mat srcImage = imread("1.jpg");
//指定 fx 和 fy ,让函数计算出目标图像的大小
resize(srcImage, dstImage, Size(), 0.5, 0.5);
完整的程序示例:
#include
#include
#include
using namespace std;
using namespace cv;
int main(int argc, char** argv)
{
//改变console字体颜色
system("color 2F");
Mat srcImage = imread("1.jpg");
Mat tempImage, dstImage1, dstImage2;
tempImage = srcImage;
imshow("原始图", srcImage);
//进行尺寸调整
resize(tempImage, dstImage1, Size(tempImage.cols / 2, tempImage.rows / 2), (0, 0), (0, 0), 3);
resize(tempImage, dstImage2, Size(tempImage.cols * 2, tempImage.rows * 2), (0, 0), (0, 0), 3);
imshow("效果图之一", dstImage1);
imshow("效果图之二", dstImage2);
waitKey(0);
return 0;
}
PyrUp() 函数的作用是向上采样并模糊一张图片,也就是放大一张图片。
void pyrUp(InputArray src,
OutputArray dst,
const Size& dstsize = Size(),
int borderType = BORDER_DEFAULT
)
● 第一个参数:InputArray 类型的 src ,输入图像。
● 第二个参数,OutputArray 类的 dst,输出图像,和源图像有一样的尺寸和类型。
● 第三个参数:const Size& 类型的 dstsize,输出图像的大小,有默认值 Size() ,即默认条件下,由 Size(src.cols * 2, src.row * 2) 来进行计算,且一直需要满足下列条件:
| dstsize.width - src.cols * 2 | <= (dstsiz.width mod 2)
| dstsize.height- src.rows* 2 | <= (dstsiz.height mod 2)
●第四个参数:int 类型的 borderType ,边界模式,一般不用管它。
首先,它通过插入可为零的行与列,对源图像进行向上取样操作,然后将结果与 pyrDown() 乘以 4 的内核做卷积。
示例程序:
#include
#include
#include
using namespace std;
using namespace cv;
int main(int argc, char** argv)
{
//改变console字体颜色
system("color 2F");
Mat srcImage = imread("1.jpg");
Mat tempImage, dstImage1;
tempImage = srcImage;
imshow("原始图", srcImage);
//进行向上取样操作
pyrUp(tempImage, dstImage1, Size(tempImage.cols * 2, tempImage.rows * 2));
imshow("效果图", dstImage1);
waitKey(0);
return 0;
}
PyrDown() 函数的作用是向下采样并模糊一张图片,也就是缩小一张图片。
void pyrDown(InputArray src,
OutputArray dst,
const Size& dstsize = Size(),
int borderType = BORDER_DEFAULT
)
● 第一个参数:InputArray 类型的 src ,输入图像。
● 第二个参数,OutputArray 类的 dst,输出图像,和源图像有一样的尺寸和类型。
● 第三个参数:const Size& 类型的 dstsize,输出图像的大小,有默认值 Size() ,即默认条件下,由 Size((src.cols + 1) / 2, (src.row + 1) / 2) 来进行计算,且一直需要满足下列条件:
| dstsize.width * 2 - src.cols | <= 2
| dstsize.height * 2 - src.rows | <= 2
该 pyrDown() 函数执行了高斯金字塔的向下采样的步骤,首先,它将源图像与如下内核做卷积运算:
接着,它通过对图像的偶数行和列做插值来进行向下采样操作。
● 第四个参数:int 类型的 borderType ,边界模式,一般不用管它。
首先,它通过插入可为零的行与列,对源图像进行向上取样操作,然后将结果与 pyrDown() 乘以 4 的内核做卷积。
完整的示例程序如下:
#include
#include
#include
using namespace std;
using namespace cv;
int main(int argc, char** argv)
{
//改变console字体颜色
system("color 2F");
Mat srcImage = imread("1.jpg");
Mat tempImage, dstImage1;
tempImage = srcImage;
imshow("原始图", srcImage);
//进行向上取样操作
pyrDown(tempImage, dstImage1, Size(tempImage.cols / 2, tempImage.rows / 2));
imshow("效果图", dstImage1);
waitKey(0);
return 0;
}
#include
#include
#include
using namespace std;
using namespace cv;
#define WINDOW_NAME "【程序窗口】" //为窗口标题定义的宏
Mat g_srcImage, g_dstImage, g_tmpImage;
void ShowHelpText()
{
//输出一些帮助信息
printf("\n\t欢迎来到OpenCV图像金字塔和resize示例程序~\n\n");
printf("\n\n\t按键操作说明: \n\n"
"\t\t键盘按键【ESC】或者【Q】- 退出程序\n"
"\t\t键盘按键【1】或者【W】- 进行基于【resize】函数的图片放大\n"
"\t\t键盘按键【2】或者【S】- 进行基于【resize】函数的图片缩小\n"
"\t\t键盘按键【3】或者【A】- 进行基于【pyrUp】函数的图片放大\n"
"\t\t键盘按键【4】或者【D】- 进行基于【pyrDown】函数的图片缩小\n"
);
}
int main()
{
//改变console字体颜色
system("color 2F");
//显示帮助文字
ShowHelpText();
//载入原图
g_srcImage = imread("1.jpg");//其尺寸需被2的N次方整除,N为可以缩放的次数
if (!g_srcImage.data) { printf("Oh,no,读取srcImage错误~! \n"); return false; }
// 创建显示窗口
namedWindow(WINDOW_NAME, WINDOW_AUTOSIZE);
imshow(WINDOW_NAME, g_srcImage);
//参数赋值
g_tmpImage = g_srcImage;
g_dstImage = g_tmpImage;
int key = 0;
//轮询获取按键信息
while (1)
{
key = waitKey(9);//读取键值到key变量中
//根据key变量的值,进行不同的操作
switch (key)
{
//======================【程序退出相关键值处理】=======================
case 27://按键ESC
return 0;
break;
case 'q'://按键Q
return 0;
break;
//======================【图片放大相关键值处理】=======================
case 'a'://按键A按下,调用pyrUp函数
pyrUp(g_tmpImage, g_dstImage, Size(g_tmpImage.cols * 2, g_tmpImage.rows * 2));
printf(">检测到按键【A】被按下,开始进行基于【pyrUp】函数的图片放大:图片尺寸×2 \n");
break;
case 'w'://按键W按下,调用resize函数
resize(g_tmpImage, g_dstImage, Size(g_tmpImage.cols * 2, g_tmpImage.rows * 2));
printf(">检测到按键【W】被按下,开始进行基于【resize】函数的图片放大:图片尺寸×2 \n");
break;
case '1'://按键1按下,调用resize函数
resize(g_tmpImage, g_dstImage, Size(g_tmpImage.cols * 2, g_tmpImage.rows * 2));
printf(">检测到按键【1】被按下,开始进行基于【resize】函数的图片放大:图片尺寸×2 \n");
break;
case '3': //按键3按下,调用pyrUp函数
pyrUp(g_tmpImage, g_dstImage, Size(g_tmpImage.cols * 2, g_tmpImage.rows * 2));
printf(">检测到按键【3】被按下,开始进行基于【pyrUp】函数的图片放大:图片尺寸×2 \n");
break;
//======================【图片缩小相关键值处理】=======================
case 'd': //按键D按下,调用pyrDown函数
pyrDown(g_tmpImage, g_dstImage, Size(g_tmpImage.cols / 2, g_tmpImage.rows / 2));
printf(">检测到按键【D】被按下,开始进行基于【pyrDown】函数的图片缩小:图片尺寸/2\n");
break;
case 's': //按键S按下,调用resize函数
resize(g_tmpImage, g_dstImage, Size(g_tmpImage.cols / 2, g_tmpImage.rows / 2));
printf(">检测到按键【S】被按下,开始进行基于【resize】函数的图片缩小:图片尺寸/2\n");
break;
case '2'://按键2按下,调用resize函数
resize(g_tmpImage, g_dstImage, Size(g_tmpImage.cols / 2, g_tmpImage.rows / 2));
printf(">检测到按键【2】被按下,开始进行基于【resize】函数的图片缩小:图片尺寸/2\n");
break;
case '4': //按键4按下,调用pyrDown函数
pyrDown(g_tmpImage, g_dstImage, Size(g_tmpImage.cols / 2, g_tmpImage.rows / 2));
printf(">检测到按键【4】被按下,开始进行基于【pyrDown】函数的图片缩小:图片尺寸/2\n");
break;
}
//经过操作后,显示变化后的图
imshow(WINDOW_NAME, g_dstImage);
//将g_dstImage赋给g_tmpImage,方便下一次循环
g_tmpImage = g_dstImage;
}
return 0;
}
阈值可以被视作最简单的图像分割方法。比如从一幅图像中利用阈值分割出我们需要的物体,这样的图像分割方法基于图像中物体与背景之间的灰度差异,而且此分割属于像素级的分割。为了从一幅图像中提取出我们需要的部分,应该用图像中的每一个像素点的灰度值与选取的阈值进行比较,并作出相应判断。
一旦找到了需要分割的物体的像素点,可以对这些像素点设定一些特定的值来表示。比如,可以将物体的像素点的灰度值设定为 0(黑色),其他像素点的灰度值为 255(白色)。最好设定两种颜色对比度较强,以方便观察结果。
在 OpenCV 中,Threshold() 函数(基本阈值操作)和 adaptiveThreshold() 函数(自适应阈值操作)可以完成这样的要求。它们的基本思想是:给定一个数组和一个阈值,然后根据数组中的每个元素的值是高于还是低于阈值而进行一些处理。
Threshold() 函数对单通道数组应用固定阈值操作,该函数的典型应用是对灰度图像进行阈值操作得到二值图像,(compare() 函数也能达到此目的)或是去掉噪声,例如过滤很小或很大像素值的图像点。
double threshold(InputArray src,
OutputArray dst,
double thresh,
double maxval,
int type
)
● 第一个参数:InputArray 类型的 src,填单通道,8 位或 32 位浮点类型的 Mat。
● 第二个参数:OutputArray 类型的 dst,用于存放输出结果。
● 第三个参数:double 类型的 thresh,阈值的具体值。
● 第四个参数:double 类型的 maxval,当第五个参数阈值类型 type 取 THRESH_BINARY 或 THRESH_BINARY_INV 时阈值类型时的最大值。
● 第五个参数:int 类型的 type,阈值类型。threshold() 函数支持的对图像取阈值的方法由其确定,具体用法如图:
上述标识符依次取值分别为 0,1,2,3,4。
而针对上述公式,一一图形化的阈值描述如图所示:(THRESH_BINARY 对应二进制阈值,依次类推)
adaptiveThreshold() 函数的作用是对矩阵采样自适应阈值操作,支持就地操作。
void adaptiveThreshold(InputArray src,
OutputArray dst,
double maxValue,
int adaptiveMethod,
int thresholdType,
int blackSize,
double C
)
● 第一个参数:InputArray 类型的 src,源图像,且需为 8 位单通道浮点型图像。
● 第二个参数:OutputArray 类型的 dst,用于存放输出结果。
● 第三个参数:double 类型的 maxValue,给像素赋的满足条件的非零值。具体看下面的讲解。
● 第四个参数:int 类型的 adaptiveMethod,用于指定要使用的自适应阈值算法,可取值为 ADAPTIVE_THRESH_MEAN_C 或 ADAPTIVE_THRESH_GAUSSIAN_C。
● 第五个参数:int 类型的 thresholdType,阈值类型。取值必须为 THRESH_BINARY 、THRESH_BINARY_INV 其中之一。
● 第六个参数:int 类型的 blockSize,用于计算阈值大小的一个像素的邻域尺寸,取值为 3、5、7 等。
● 第七个参数:double 类型的 C,减去平均或加权平均值后的常数值。通常其为正数,但少数情况下也可以为零或负数。
adaptiveThreshold() 函数根据如下公式,将一幅灰度图变换为一幅二值图。
当第五个参数 “阈值类型” thresholdType 取值为 THRESH_BINART 时,公式如下:
当第五个参数 “阈值类型” thresholdType 取值为 THRESH_BINART_INV 时,公式为:
而其中的 T(x, y) 分别计算每个单独像素的阈值,取值如下:
● 对于 ADAPTIVE_THRESH_MEAN_C 方法,阈值 T(x,y) 为 blockSize x blockSize 邻域内 (x,y) 减去第七个参数 C 的平均值。
● 对于 ADAPTIVE_THRESH_GAUSSIAN_C 方法,阈值 T(x,y) 为 blockSize x blockSize 邻域内 (x,y) 减去第七个参数 C 与高斯窗交叉相关的加权总和。
#include
#include
#include
using namespace std;
using namespace cv;
#define WINDOW_NAME "【程序窗口】" //为窗口标题定义的宏
int g_nThresholdValue = 100;
int g_nThresholdType = 3;
Mat g_srcImage, g_grayImage, g_dstImage;
void on_Threshold(int, void*)
{
//调用阈值函数
threshold(g_grayImage, g_dstImage, g_nThresholdValue, 255, g_nThresholdType);
//更新效果图
imshow(WINDOW_NAME, g_dstImage);
}
static void ShowHelpText()
{
//输出一些帮助信息
printf("\n\t欢迎来到【基本阈值操作】示例程序~\n\n");
printf("\n\t按键操作说明: \n\n"
"\t\t键盘按键【ESC】- 退出程序\n"
"\t\t滚动条模式0- 二进制阈值\n"
"\t\t滚动条模式1- 反二进制阈值\n"
"\t\t滚动条模式2- 截断阈值\n"
"\t\t滚动条模式3- 反阈值化为0\n"
"\t\t滚动条模式4- 阈值化为0\n");
}
int main()
{
//【0】改变console字体颜色
system("color 1F");
//【0】显示欢迎和帮助文字
ShowHelpText();
//【1】读入源图片
g_srcImage = imread("1.jpg");
if (!g_srcImage.data)
{
printf("读取图片错误,请确定目录下是否有imread函数指定的图片存在~! \n"); return false;
}
imshow("原始图", g_srcImage);
//【2】存留一份原图的灰度图
cvtColor(g_srcImage, g_grayImage, COLOR_RGB2GRAY);
//【3】创建窗口并显示原始图
namedWindow(WINDOW_NAME, WINDOW_AUTOSIZE);
//【4】创建滑动条来控制阈值
createTrackbar("模式",
WINDOW_NAME, &g_nThresholdType,
4, on_Threshold);
createTrackbar("参数值",
WINDOW_NAME, &g_nThresholdValue,
255, on_Threshold);
//【5】初始化自定义的阈值回调函数
on_Threshold(0, 0);
// 【6】轮询等待用户按键,如果ESC键按下则退出程序
while (1)
{
int key;
key = waitKey(20);
if ((char)key == 27) { break; }
}
}
原始图: