OpenCV之imgproc 模块. 图像处理(1)图像平滑处理 腐蚀与膨胀(Eroding and Dilating) 更多形态学变换 图像金字塔 基本的阈值操作
图像平滑处理
目标
本教程教您怎样使用各种线性滤波器对图像进行平滑处理,相关OpenCV函数如下:
- blur
- GaussianBlur
- medianBlur
- bilateralFilter
原理
Note
以下原理来源于Richard Szeliski 的著作 Computer Vision: Algorithms and Applications 以及 Learning OpenCV
平滑 也称 模糊, 是一项简单且使用频率很高的图像处理方法。
平滑处理的用途有很多, 但是在本教程中我们仅仅关注它减少噪声的功用 (其他用途在以后的教程中会接触到)。
平滑处理时需要用到一个 滤波器 。 最常用的滤波器是 线性 滤波器,线性滤波处理的输出像素值 (i.e.
) 是输入像素值 (i.e.
)的加权和 :
称为 核, 它仅仅是一个加权系数。
不妨把 滤波器 想象成一个包含加权系数的窗口,当使用这个滤波器平滑处理图像时,就把这个窗口滑过图像。
滤波器的种类有很多, 这里仅仅提及最常用的:
归一化块滤波器 (Normalized Box Filter)
最简单的滤波器, 输出像素值是核窗口内像素值的 均值 ( 所有像素加权系数相等)
核如下:
高斯滤波器 (Gaussian Filter)
最有用的滤波器 (尽管不是最快的)。 高斯滤波是将输入数组的每一个像素点与 高斯内核 卷积将卷积和当作输出像素值。
还记得1维高斯函数的样子吗?
假设图像是1维的,那么观察上图,不难发现中间像素的加权系数是最大的, 周边像素的加权系数随着它们远离中间像素的距离增大而逐渐减小。
Note
2维高斯函数可以表达为 :
其中 为均值 (峰值对应位置),
代表标准差 (变量
和 变量
各有一个均值,也各有一个标准差)
中值滤波器 (Median Filter)
中值滤波将图像的每个像素用邻域 (以当前像素为中心的正方形区域)像素的 中值 代替 。
双边滤波 (Bilateral Filter)
- 目前我们了解的滤波器都是为了 平滑 图像, 问题是有些时候这些滤波器不仅仅削弱了噪声, 连带着把边缘也给磨掉了。 为避免这样的情形 (至少在一定程度上 ), 我们可以使用双边滤波。
- 类似于高斯滤波器,双边滤波器也给每一个邻域像素分配一个加权系数。 这些加权系数包含两个部分, 第一部分加权方式与高斯滤波一样,第二部分的权重则取决于该邻域像素与当前像素的灰度差值。
- 详细的解释可以查看 链接
源码
本程序做什么?
- 装载一张图像
- 使用4种不同滤波器 (见原理部分) 并显示平滑图像
下载代码: 点击 这里
代码一瞥:
#include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp"using namespace std; using namespace cv;/// 全局变量 int DELAY_CAPTION = 1500; int DELAY_BLUR = 100; int MAX_KERNEL_LENGTH = 31;Mat src; Mat dst; char window_name[] = "Filter Demo 1";/// 函数申明 int display_caption( char* caption ); int display_dst( int delay );/** * main 函数 */int main( int argc, char** argv ){namedWindow( window_name, CV_WINDOW_AUTOSIZE );/// 载入原图像src = imread( "../images/lena.jpg", 1 );if( display_caption( "Original Image" ) != 0 ) { return 0; }dst = src.clone();if( display_dst( DELAY_CAPTION ) != 0 ) { return 0; }/// 使用 均值平滑if( display_caption( "Homogeneous Blur" ) != 0 ) { return 0; }for ( int i = 1; i < MAX_KERNEL_LENGTH; i = i + 2 ){ blur( src, dst, Size( i, i ), Point(-1,-1) );if( display_dst( DELAY_BLUR ) != 0 ) { return 0; } }/// 使用高斯平滑if( display_caption( "Gaussian Blur" ) != 0 ) { return 0; }for ( int i = 1; i < MAX_KERNEL_LENGTH; i = i + 2 ){ GaussianBlur( src, dst, Size( i, i ), 0, 0 );if( display_dst( DELAY_BLUR ) != 0 ) { return 0; } }/// 使用中值平滑if( display_caption( "Median Blur" ) != 0 ) { return 0; }for ( int i = 1; i < MAX_KERNEL_LENGTH; i = i + 2 ){ medianBlur ( src, dst, i );if( display_dst( DELAY_BLUR ) != 0 ) { return 0; } }/// 使用双边平滑if( display_caption( "Bilateral Blur" ) != 0 ) { return 0; }for ( int i = 1; i < MAX_KERNEL_LENGTH; i = i + 2 ){ bilateralFilter ( src, dst, i, i*2, i/2 );if( display_dst( DELAY_BLUR ) != 0 ) { return 0; } }/// 等待用户输入display_caption( "End: Press a key!" );waitKey(0);return 0;}int display_caption( char* caption ){dst = Mat::zeros( src.size(), src.type() );putText( dst, caption,Point( src.cols/4, src.rows/2),CV_FONT_HERSHEY_COMPLEX, 1, Scalar(255, 255, 255) );imshow( window_name, dst );int c = waitKey( DELAY_CAPTION );if( c >= 0 ) { return -1; }return 0;}int display_dst( int delay ){imshow( window_name, dst );int c = waitKey ( delay );if( c >= 0 ) { return -1; }return 0;}
解释
下面看一看有关平滑的OpenCV函数,其余部分大家已经很熟了。
归一化块滤波器:
OpenCV函数 blur 执行了归一化块平滑操作。
for ( int i = 1; i < MAX_KERNEL_LENGTH; i = i + 2 ){ blur( src, dst, Size( i, i ), Point(-1,-1) );if( display_dst( DELAY_BLUR ) != 0 ) { return 0; } }
我们输入4个实参 (详细的解释请参考 Reference):
- src: 输入图像
- dst: 输出图像
- Size( w,h ): 定义内核大小( w 像素宽度, h 像素高度)
- Point(-1, -1): 指定锚点位置(被平滑点), 如果是负值,取核的中心为锚点。
高斯滤波器:
OpenCV函数 GaussianBlur 执行高斯平滑 :
for ( int i = 1; i < MAX_KERNEL_LENGTH; i = i + 2 ){ GaussianBlur( src, dst, Size( i, i ), 0, 0 );if( display_dst( DELAY_BLUR ) != 0 ) { return 0; } }
我们输入4个实参 (详细的解释请参考 Reference):
- src: 输入图像
- dst: 输出图像
- Size(w, h): 定义内核的大小(需要考虑的邻域范围)。
和
必须是正奇数,否则将使用
和
参数来计算内核大小。
: x 方向标准方差, 如果是
则
使用内核大小计算得到。
: y 方向标准方差, 如果是
则
使用内核大小计算得到。.
中值滤波器:
OpenCV函数 medianBlur 执行中值滤波操作:
for ( int i = 1; i < MAX_KERNEL_LENGTH; i = i + 2 ){ medianBlur ( src, dst, i );if( display_dst( DELAY_BLUR ) != 0 ) { return 0; } }
我们用了3个参数:
- src: 输入图像
- dst: 输出图像, 必须与 src 相同类型
- i: 内核大小 (只需一个值,因为我们使用正方形窗口),必须为奇数。
双边滤波器
OpenCV函数 bilateralFilter 执行双边滤波操作:
for ( int i = 1; i < MAX_KERNEL_LENGTH; i = i + 2 ){ bilateralFilter ( src, dst, i, i*2, i/2 );if( display_dst( DELAY_BLUR ) != 0 ) { return 0; } }
我们使用了5个参数:
- src: 输入图像
- dst: 输出图像
- d: 像素的邻域直径
: 颜色空间的标准方差
: 坐标空间的标准方差(像素单位)
结果
程序显示了原始图像( lena.jpg) 和使用4种滤波器之后的效果图。
这里显示的是使用 中值滤波 之后的效果图:
腐蚀与膨胀(Eroding and Dilating)
目标
本文档尝试解答如下问题:
- 如何使用OpenCV提供的两种最基本的形态学操作,腐蚀与膨胀( Erosion 与 Dilation):
- erode
- dilate
原理
Note
以下内容来自于Bradski和Kaehler的大作: Learning OpenCV .
形态学操作
简单来讲,形态学操作就是基于形状的一系列图像处理操作。通过将 结构元素 作用于输入图像来产生输出图像。
最基本的形态学操作有二:腐蚀与膨胀(Erosion 与 Dilation)。 他们的运用广泛:
- 消除噪声
- 分割(isolate)独立的图像元素,以及连接(join)相邻的元素。
- 寻找图像中的明显的极大值区域或极小值区域。
通过以下图像,我们简要来讨论一下膨胀与腐蚀操作(译者注:注意这张图像中的字母为黑色,背景为白色,而不是一般意义的背景为黑色,前景为白色):
膨胀
此操作将图像
与任意形状的内核 (
),通常为正方形或圆形,进行卷积。
内核
有一个可定义的 锚点, 通常定义为内核中心点。
进行膨胀操作时,将内核
划过图像,将内核
覆盖区域的最大相素值提取,并代替锚点位置的相素。显然,这一最大化操作将会导致图像中的亮区开始”扩展” (因此有了术语膨胀 dilation )。对上图采用膨胀操作我们得到:
背景(白色)膨胀,而黑色字母缩小了。
腐蚀
腐蚀在形态学操作家族里是膨胀操作的孪生姐妹。它提取的是内核覆盖下的相素最小值。
进行腐蚀操作时,将内核
划过图像,将内核
覆盖区域的最小相素值提取,并代替锚点位置的相素。
以与膨胀相同的图像作为样本,我们使用腐蚀操作。从下面的结果图我们看到亮区(背景)变细,而黑色区域(字母)则变大了。
源码
下面是本教程的源码, 你也可以从 here 下载。
#include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include "highgui.h" #include <stdlib.h> #include <stdio.h>using namespace cv;/// 全局变量 Mat src, erosion_dst, dilation_dst;int erosion_elem = 0; int erosion_size = 0; int dilation_elem = 0; int dilation_size = 0; int const max_elem = 2; int const max_kernel_size = 21;/** Function Headers */ void Erosion( int, void* ); void Dilation( int, void* );/** @function main */ int main( int argc, char** argv ) {/// Load 图像src = imread( argv[1] );if( !src.data ){ return -1; }/// 创建显示窗口namedWindow( "Erosion Demo", CV_WINDOW_AUTOSIZE );namedWindow( "Dilation Demo", CV_WINDOW_AUTOSIZE );cvMoveWindow( "Dilation Demo", src.cols, 0 );/// 创建腐蚀 TrackbarcreateTrackbar( "Element:\n 0: Rect \n 1: Cross \n 2: Ellipse", "Erosion Demo",&erosion_elem, max_elem,Erosion );createTrackbar( "Kernel size:\n 2n +1", "Erosion Demo",&erosion_size, max_kernel_size,Erosion );/// 创建膨胀 TrackbarcreateTrackbar( "Element:\n 0: Rect \n 1: Cross \n 2: Ellipse", "Dilation Demo",&dilation_elem, max_elem,Dilation );createTrackbar( "Kernel size:\n 2n +1", "Dilation Demo",&dilation_size, max_kernel_size,Dilation );/// Default startErosion( 0, 0 );Dilation( 0, 0 );waitKey(0);return 0; }/** @function Erosion */ void Erosion( int, void* ) {int erosion_type;if( erosion_elem == 0 ){ erosion_type = MORPH_RECT; }else if( erosion_elem == 1 ){ erosion_type = MORPH_CROSS; }else if( erosion_elem == 2) { erosion_type = MORPH_ELLIPSE; }Mat element = getStructuringElement( erosion_type,Size( 2*erosion_size + 1, 2*erosion_size+1 ),Point( erosion_size, erosion_size ) );/// 腐蚀操作erode( src, erosion_dst, element );imshow( "Erosion Demo", erosion_dst ); }/** @function Dilation */ void Dilation( int, void* ) {int dilation_type;if( dilation_elem == 0 ){ dilation_type = MORPH_RECT; }else if( dilation_elem == 1 ){ dilation_type = MORPH_CROSS; }else if( dilation_elem == 2) { dilation_type = MORPH_ELLIPSE; }Mat element = getStructuringElement( dilation_type,Size( 2*dilation_size + 1, 2*dilation_size+1 ),Point( dilation_size, dilation_size ) );///膨胀操作dilate( src, dilation_dst, element );imshow( "Dilation Demo", dilation_dst ); }
解释
大部分代码应该不需要解释了 (如果有任何疑问,请回头参考前面的教程)。 让我们来回顾一下本程序的总体流程:
- 装载图像 (可以是 RGB图像或者灰度图 )
- 创建两个显示窗口 (一个用于膨胀输出,一个用于腐蚀输出)
- 为每个操作创建两个 Trackbars:
- 第一个 trackbar “Element” 返回 erosion_elem 或者 dilation_elem
- 第二个 trackbar “Kernel size” 返回 erosion_size 或者 dilation_size 。
- 每次移动标尺, 用户函数 Erosion 或者 Dilation 就会被调用,函数将根据当前的trackbar位置更新输出图像。
让我们分析一下这两个函数:
Erosion:
/** @function Erosion */ void Erosion( int, void* ) {int erosion_type;if( erosion_elem == 0 ){ erosion_type = MORPH_RECT; }else if( erosion_elem == 1 ){ erosion_type = MORPH_CROSS; }else if( erosion_elem == 2) { erosion_type = MORPH_ELLIPSE; }Mat element = getStructuringElement( erosion_type,Size( 2*erosion_size + 1, 2*erosion_size+1 ),Point( erosion_size, erosion_size ) );/// 腐蚀操作erode( src, erosion_dst, element );imshow( "Erosion Demo", erosion_dst ); }
进行 腐蚀 操作的函数是 erode 。 它接受了三个参数:
src: 原图像
erosion_dst: 输出图像
element: 腐蚀操作的内核。 如果不指定,默认为一个简单的
矩阵。否则,我们就要明确指定它的形状,可以使用函数 getStructuringElement:
Mat element = getStructuringElement( erosion_type,Size( 2*erosion_size + 1, 2*erosion_size+1 ),Point( erosion_size, erosion_size ) );
我们可以为我们的内核选择三种形状之一:
- 矩形: MORPH_RECT
- 交叉形: MORPH_CROSS
- 椭圆形: MORPH_ELLIPSE
然后,我们还需要指定内核大小,以及 锚点 位置。不指定锚点位置,则默认锚点在内核中心位置。
就这些了,我们现在可以对图像进行腐蚀操作了。
Note
OpenCV的 erode 函数还有另外的参数,其中一个参数允许你一下对图像进行多次腐蚀操作。在这个简单的文档中没有用到它,但是你可以参考OpenCV的使用手册。
Dilation:
下面是膨胀的代码,你可以看到,它和 Erosion 函数是多么相似。 这里我们同样可以指定内核的形状,锚点和大小。
/** @function Dilation */ void Dilation( int, void* ) {int dilation_type;if( dilation_elem == 0 ){ dilation_type = MORPH_RECT; }else if( dilation_elem == 1 ){ dilation_type = MORPH_CROSS; }else if( dilation_elem == 2) { dilation_type = MORPH_ELLIPSE; }Mat element = getStructuringElement( dilation_type,Size( 2*dilation_size + 1, 2*dilation_size+1 ),Point( dilation_size, dilation_size ) );/// 膨胀操作dilate( src, dilation_dst, element );imshow( "Dilation Demo", dilation_dst ); }
结果
编译并使用图像路径作为参数运行程序,比如我们使用以下图像:
下面是操作的结果。 更改Trackbars的位置就会产生不一样的输出图像,自己试试吧。 最后,你还可以通过增加第三个Trackbar来控制膨胀或腐蚀的次数。
更多形态学变换
目标
本文档尝试解答如下问题:
- 如何使用OpenCV函数 morphologyEx 进行形态学操作:
- 开运算 (Opening)
- 闭运算 (Closing)
- 形态梯度 (Morphological Gradient)
- 顶帽 (Top Hat)
- 黑帽(Black Hat)
原理
Note
以下内容来自于Bradski和Kaehler的大作 Learning OpenCV 。
前一节我们讨论了两种最基本的形态学操作:
- 腐蚀 (Erosion)
- 膨胀 (Dilation)
运用这两个基本操作,我们可以实现更高级的形态学变换。这篇文档将会简要介绍OpenCV提供的5种高级形态学操作:
开运算 (Opening)
开运算是通过先对图像腐蚀再膨胀实现的。
能够排除小团块物体(假设物体较背景明亮)
请看下面,左图是原图像,右图是采用开运算转换之后的结果图。 观察发现字母拐弯处的白色空间消失。
闭运算(Closing)
闭运算是通过先对图像膨胀再腐蚀实现的。
能够排除小型黑洞(黑色区域)。
形态梯度(Morphological Gradient)
膨胀图与腐蚀图之差
能够保留物体的边缘轮廓,如下所示:
顶帽(Top Hat)
原图像与开运算结果图之差
黑帽(Black Hat)
闭运算结果图与原图像之差
源码
下面是本教程的源码, 你也可以从 这里 下载。
#include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <stdlib.h> #include <stdio.h>using namespace cv;/// 全局变量 Mat src, dst;int morph_elem = 0; int morph_size = 0; int morph_operator = 0; int const max_operator = 4; int const max_elem = 2; int const max_kernel_size = 21;char* window_name = "Morphology Transformations Demo";/** 回调函数申明 */ void Morphology_Operations( int, void* );/** @函数 main */ int main( int argc, char** argv ) {/// 装载图像src = imread( argv[1] );if( !src.data ){ return -1; }/// 创建显示窗口namedWindow( window_name, CV_WINDOW_AUTOSIZE );/// 创建选择具体操作的 trackbarcreateTrackbar("Operator:\n 0: Opening - 1: Closing \n 2: Gradient - 3: Top Hat \n 4: Black Hat", window_name, &morph_operator, max_operator, Morphology_Operations );/// 创建选择内核形状的 trackbarcreateTrackbar( "Element:\n 0: Rect - 1: Cross - 2: Ellipse", window_name,&morph_elem, max_elem,Morphology_Operations );/// 创建选择内核大小的 trackbarcreateTrackbar( "Kernel size:\n 2n +1", window_name,&morph_size, max_kernel_size,Morphology_Operations );/// 启动使用默认值Morphology_Operations( 0, 0 );waitKey(0);return 0;}/** * @函数 Morphology_Operations */ void Morphology_Operations( int, void* ) {// 由于 MORPH_X的取值范围是: 2,3,4,5 和 6int operation = morph_operator + 2;Mat element = getStructuringElement( morph_elem, Size( 2*morph_size + 1, 2*morph_size+1 ), Point( morph_size, morph_size ) );/// 运行指定形态学操作morphologyEx( src, dst, operation, element );imshow( window_name, dst );}
解释
看一下程序的总体流程:
装载图像
创建显示形态学操作的窗口
创建3个trackbar获取用户参数:
第一个trackbar “Operator” 返回用户选择的形态学操作类型 (morph_operator).
createTrackbar("Operator:\n 0: Opening - 1: Closing \n 2: Gradient - 3: Top Hat \n 4: Black Hat",window_name, &morph_operator, max_operator,Morphology_Operations );
第二个trackbar “Element” 返回 morph_elem, 指定内核形状:
createTrackbar( "Element:\n 0: Rect - 1: Cross - 2: Ellipse", window_name,&morph_elem, max_elem,Morphology_Operations );
第三个trackbar “Kernel Size” 返回内核大小(morph_size)
createTrackbar( "Kernel size:\n 2n +1", window_name,&morph_size, max_kernel_size,Morphology_Operations );
每当任一标尺被移动, 用户函数 Morphology_Operations 就会被调用,该函数获取trackbar的当前值运行指定操作并更新显示结果图像。
/** * @函数 Morphology_Operations */ void Morphology_Operations( int, void* ) {// 由于 MORPH_X的取值范围是: 2,3,4,5 和 6int operation = morph_operator + 2;Mat element = getStructuringElement( morph_elem, Size( 2*morph_size + 1, 2*morph_size+1 ), Point( morph_size, morph_size ) );/// 运行指定形态学操作morphologyEx( src, dst, operation, element );imshow( window_name, dst );}
运行形态学操作的核心函数是 morphologyEx 。在本例中,我们使用了4个参数(其余使用默认值):
- src : 原 (输入) 图像
- dst: 输出图像
- operation: 需要运行的形态学操作。 我们有5个选项:
- Opening: MORPH_OPEN : 2
- Closing: MORPH_CLOSE: 3
- Gradient: MORPH_GRADIENT: 4
- Top Hat: MORPH_TOPHAT: 5
- Black Hat: MORPH_BLACKHAT: 6
你可以看到, 它们的取值范围是 <2-6>, 因此我们要将从tracker获取的值增加(+2):
int operation = morph_operator + 2;
- element: 内核,可以使用函数:get_structuring_element:getStructuringElement <> 自定义。
结果
在编译上面的代码之后, 我们可以运行结果,将图片路径输入。这里使用图像: baboon.png:
这里是显示窗口的两个截图。第一幅图显示了使用交错内核和 开运算 之后的结果, 第二幅图显示了使用椭圆内核和 黑帽 之后的结果。
图像金字塔
目标
本文档尝试解答如下问题:
- 如何使用OpenCV函数 pyrUp 和 pyrDown 对图像进行向上和向下采样。
原理
Note
以下内容来自于Bradski和Kaehler的大作: Learning OpenCV 。
- 当我们需要将图像转换到另一个尺寸的时候, 有两种可能:
- 放大 图像 或者
- 缩小 图像。
- 尽管OpenCV 几何变换 部分提供了一个真正意义上的图像缩放函数(resize, 在以后的教程中会学到),不过在本篇我们首先学习一下使用 图像金字塔 来做图像缩放, 图像金字塔是视觉运用中广泛采用的一项技术。
图像金字塔
- 一个图像金字塔是一系列图像的集合 - 所有图像来源于同一张原始图像 - 通过梯次向下采样获得,直到达到某个终止条件才停止采样。
- 有两种类型的图像金字塔常常出现在文献和应用中:
- 高斯金字塔(Gaussian pyramid): 用来向下采样
- 拉普拉斯金字塔(Laplacian pyramid): 用来从金字塔低层图像重建上层未采样图像
- 在这篇文档中我们将使用 高斯金字塔 。
高斯金字塔
想想金字塔为一层一层的图像,层级越高,图像越小。
每一层都按从下到上的次序编号, 层级
(表示为
尺寸小于层级
(
))。
为了获取层级为
的金字塔图像,我们采用如下方法:
将
与高斯内核卷积:
将所有偶数行和列去除。
显而易见,结果图像只有原图的四分之一。通过对输入图像
(原始图像) 不停迭代以上步骤就会得到整个金字塔。
以上过程描述了对图像的向下采样,如果将图像变大呢?:
- 首先,将图像在每个方向扩大为原来的两倍,新增的行和列以0填充(
)
- 使用先前同样的内核(乘以4)与放大后的图像卷积,获得 “新增像素” 的近似值。
- 首先,将图像在每个方向扩大为原来的两倍,新增的行和列以0填充(
这两个步骤(向下和向上采样) 分别通过OpenCV函数 pyrUp 和 pyrDown 实现, 我们将会在下面的示例中演示如何使用这两个函数。
Note
我们向下采样缩小图像的时候, 我们实际上 丢失 了一些信息。
源码
本教程的源码如下,你也可以从 这里 下载
#include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <math.h> #include <stdlib.h> #include <stdio.h>using namespace cv;/// 全局变量 Mat src, dst, tmp; char* window_name = "Pyramids Demo";/** * @函数 main */ int main( int argc, char** argv ) {/// 指示说明printf( "\n Zoom In-Out demo \n " );printf( "------------------ \n" );printf( " * [u] -> Zoom in \n" );printf( " * [d] -> Zoom out \n" );printf( " * [ESC] -> Close program \n \n" );/// 测试图像 - 尺寸必须能被 2^{n} 整除src = imread( "../images/chicky_512.jpg" );if( !src.data ){ printf(" No data! -- Exiting the program \n");return -1; }tmp = src;dst = tmp;/// 创建显示窗口namedWindow( window_name, CV_WINDOW_AUTOSIZE );imshow( window_name, dst );/// 循环while( true ){int c;c = waitKey(10);if( (char)c == 27 ){ break; }if( (char)c == 'u' ){ pyrUp( tmp, dst, Size( tmp.cols*2, tmp.rows*2 ) );printf( "** Zoom In: Image x 2 \n" );}else if( (char)c == 'd' ){ pyrDown( tmp, dst, Size( tmp.cols/2, tmp.rows/2 ) );printf( "** Zoom Out: Image / 2 \n" );}imshow( window_name, dst );tmp = dst;}return 0; }
解释
让我们来回顾一下本程序的总体流程:
装载图像(此处路径由程序设定,用户无需将图像路径当作参数输入)
/// 测试图像 - 尺寸必须能被 2^{n} 整除 src = imread( "../images/chicky_512.jpg" ); if( !src.data ){ printf(" No data! -- Exiting the program \n");return -1; }
创建两个Mat实例, 一个用来储存操作结果(dst), 另一个用来存储零时结果(tmp)。
Mat src, dst, tmp; /* ... */ tmp = src; dst = tmp;
创建窗口显示结果
namedWindow( window_name, CV_WINDOW_AUTOSIZE ); imshow( window_name, dst );
执行无限循环,等待用户输入。
while( true ) {int c;c = waitKey(10);if( (char)c == 27 ){ break; }if( (char)c == 'u' ){ pyrUp( tmp, dst, Size( tmp.cols*2, tmp.rows*2 ) );printf( "** Zoom In: Image x 2 \n" );}else if( (char)c == 'd' ){ pyrDown( tmp, dst, Size( tmp.cols/2, tmp.rows/2 ) );printf( "** Zoom Out: Image / 2 \n" );}imshow( window_name, dst );tmp = dst; }
如果用户按 ESC 键程序退出。 此外,它还提供两个选项:
向上采样 (按 ‘u’)
pyrUp( tmp, dst, Size( tmp.cols*2, tmp.rows*2 )
函数 pyrUp 接受了3个参数:
- tmp: 当前图像, 初始化为原图像 src 。
- dst: 目的图像( 显示图像,为输入图像的两倍)
- Size( tmp.cols*2, tmp.rows*2 ) : 目的图像大小, 既然我们是向上采样, pyrUp 期待一个两倍于输入图像( tmp )的大小。
向下采样(按 ‘d’)
pyrDown( tmp, dst, Size( tmp.cols/2, tmp.rows/2 )
类似于 pyrUp, 函数 pyrDown 也接受了3个参数:
- tmp: 当前图像, 初始化为原图像 src 。
- dst: 目的图像( 显示图像,为输入图像的一半)
- Size( tmp.cols/2, tmp.rows/2 ) :目的图像大小, 既然我们是向下采样, pyrDown 期待一个一半于输入图像( tmp)的大小。
注意输入图像的大小(在两个方向)必须是2的冥,否则,将会显示错误。
最后,将输入图像 tmp 更新为当前显示图像, 这样后续操作将作用于更新后的图像。
tmp = dst;
结果
在编译上面的代码之后, 我们可以运行结果。 程序调用了图像 chicky_512.jpg ,你可以在 tutorial_code/image 文件夹找到它。 注意图像大小是
, 因此向下采样不会产生错误(
)。 原图像如下所示:
首先按两次 ‘d’ 连续两次向下采样 pyrDown ,结果如图:
由于我们缩小了图像,我们也因此丢失了一些信息。通过连续按两次 ‘u’ 向上采样两次 pyrUp ,很明显图像有些失真:
基本的阈值操作
目标:
本节简介:
- OpenCV中的阈值(threshold)函数: threshold 的运用。
基本理论:
- 注意:
- 本节的解释出自Bradski与Kaehler的书籍 Learning OpenCV 。
什么是阈值?
最简单的图像分割的方法。
应用举例:从一副图像中利用阈值分割出我们需要的物体部分(当然这里的物体可以是一部分或者整体)。这样的图像分割方法是基于图像中物体与背景之间的灰度差异,而且此分割属于像素级的分割。
为了从一副图像中提取出我们需要的部分,应该用图像中的每一个像素点的灰度值与选取的阈值进行比较,并作出相应的判断。(注意:阈值的选取依赖于具体的问题。即:物体在不同的图像中有可能会有不同的灰度值。
一旦找到了需要分割的物体的像素点,我们可以对这些像素点设定一些特定的值来表示。(例如:可以将该物体的像素点的灰度值设定为:‘0’(黑色),其他的像素点的灰度值为:‘255’(白色);当然像素点的灰度值可以任意,但最好设定的两种颜色对比度较强,方便观察结果)。
阈值化的类型:
OpenCV中提供了阈值(threshold)函数: threshold 。
这个函数有5种阈值化类型,在接下来的章节中将会具体介绍。
为了解释阈值分割的过程,我们来看一个简单有关像素灰度的图片,该图如下。该图中的蓝色水平线代表着具体的一个阈值。
阈值类型1:二进制阈值化
该阈值化类型如下式所示:
解释:在运用该阈值类型的时候,先要选定一个特定的阈值量,比如:125,这样,新的阈值产生规则可以解释为大于125的像素点的灰度值设定为最大值(如8位灰度值最大为255),灰度值小于125的像素点的灰度值设定为0。
阈值类型2:反二进制阈值化
该阈值类型如下式所示:
解释:该阈值化与二进制阈值化相似,先选定一个特定的灰度值作为阈值,不过最后的设定值相反。(在8位灰度图中,例如大于阈值的设定为0,而小于该阈值的设定为255)。
阈值类型3:截断阈值化
该阈值化类型如下式所示:
解释:同样首先需要选定一个阈值,图像中大于该阈值的像素点被设定为该阈值,小于该阈值的保持不变。(例如:阈值选取为125,那小于125的阈值不改变,大于125的灰度值(230)的像素点就设定为该阈值)。
阈值类型4:阈值化为0
该阈值类型如下式所示:
解释:先选定一个阈值,然后对图像做如下处理:1 像素点的灰度值大于该阈值的不进行任何改变;2 像素点的灰度值小于该阈值的,其灰度值全部变为0。
阈值类型5:反阈值化为0
该阈值类型如下式所示:
解释:原理类似于0阈值,但是在对图像做处理的时候相反,即:像素点的灰度值小于该阈值的不进行任何改变,而大于该阈值的部分,其灰度值全部变为0。
代码示范:
简单的代码如下。同样也可以在网站中 下载 以下代码。
#include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <stdlib.h> #include <stdio.h>using namespace cv;/// 全局变量定义及赋值int threshold_value = 0; int threshold_type = 3;; int const max_value = 255; int const max_type = 4; int const max_BINARY_value = 255;Mat src, src_gray, dst; char* window_name = "Threshold Demo";char* trackbar_type = "Type: \n 0: Binary \n 1: Binary Inverted \n 2: Truncate \n 3: To Zero \n 4: To Zero Inverted"; char* trackbar_value = "Value";/// 自定义函数声明 void Threshold_Demo( int, void* );/** * @主函数 */ int main( int argc, char** argv ) {/// 读取一副图片,不改变图片本身的颜色类型(该读取方式为DOS运行模式)src = imread( argv[1], 1 );/// 将图片转换成灰度图片cvtColor( src, src_gray, CV_RGB2GRAY );/// 创建一个窗口显示图片namedWindow( window_name, CV_WINDOW_AUTOSIZE );/// 创建滑动条来控制阈值createTrackbar( trackbar_type,window_name, &threshold_type,max_type, Threshold_Demo );createTrackbar( trackbar_value,window_name, &threshold_value,max_value, Threshold_Demo );/// 初始化自定义的阈值函数Threshold_Demo( 0, 0 );/// 等待用户按键。如果是ESC健则退出等待过程。while(true){int c;c = waitKey( 20 );if( (char)c == 27 ){ break; }}}/** * @自定义的阈值函数 */ void Threshold_Demo( int, void* ) {/* 0: 二进制阈值 1: 反二进制阈值 2: 截断阈值 3: 0阈值 4: 反0阈值 */threshold( src_gray, dst, threshold_value, max_BINARY_value,threshold_type );imshow( window_name, dst ); }
解释:
先看一下整个程序的结构:
先读取一副图片,如果是图片颜色类型是RGB3色类型,则转换成灰度类型的图像。转换颜色类型可以运用OpenCV中的 cvtColor<> 函数。
src = imread( argv[1], 1 );/// 颜色类型从RGB 转换成灰度 cvtColor( src, src_gray, CV_RGB2GRAY );
然后创建一个窗口来显示该图片可以检验转换结果
namedWindow( window_name, CV_WINDOW_AUTOSIZE );
接着该程序创建两个滚动条来等待用户的输入:
- 第一个滚动条作用:选择阈值类型:二进制,反二进制,截断,0,反0。
- 第二个滚动条作用:选择阈值的大小。
createTrackbar( trackbar_type,window_name, &threshold_type,max_type, Threshold_Demo );createTrackbar( trackbar_value,window_name, &threshold_value,max_value, Threshold_Demo );
在这里等到用户拖动滚动条来输入阈值类型以及阈值的大小,或者是用户键入ESC健退出程序。
无论何时拖动滚动条,用户自定义的阈值函数都将会被调用。
/** * @自定义的阈值函数 */ void Threshold_Demo( int, void* ) {/* 0: 二进制阈值 1: 反二进制阈值 2: 截断阈值 3: 0阈值 4: 反0阈值 */threshold( src_gray, dst, threshold_value, max_BINARY_value,threshold_type );imshow( window_name, dst ); }
就像你看到的那样,在这样的过程中,函数 threshold<> 会接受到5个参数:
- src_gray: 输入的灰度图像的地址。
- dst: 输出图像的地址。
- threshold_value: 进行阈值操作时阈值的大小。
- max_BINARY_value: 设定的最大灰度值(该参数运用在二进制与反二进制阈值操作中)。
- threshold_type: 阈值的类型。从上面提到的5种中选择出的结果。
结果:
程序编译过后,从正确的路径中读取一张图片。例如,该输入图片如下所示:
首先,阈值类型选择为反二进制阈值类型。我们希望灰度值大于阈值的变暗,即这一部分像素的灰度值设定为0。从下图中可以很清楚的看到这样的变化。(在原图中,狗的嘴和眼睛部分比图像中的其他部分要亮,在结果图中可以看到由于反二进制阈值分割,这两部分变的比其他图像的都要暗。原理具体参见本节中反二进制阈值部分解释)
现在,阈值的类型选择为0阈值。在这种情况下,我们希望那些在图像中最黑的像素点彻底的变成黑色,而其他大于阈值的像素保持原来的面貌。其结果如下图所示:
OpenCV之imgproc 模块. 图像处理(1)图像平滑处理 腐蚀与膨胀(Eroding and Dilating) 更多形态学变换 图像金字塔 基本的阈值操作相关推荐
- OpenCV 【十八】图像平滑处理/腐蚀与膨胀(Eroding and Dilating)/开闭运算,形态梯度,顶帽,黑帽运算
图像滤波总结(面试经验总结)https://blog.csdn.net/Darlingqiang/article/details/79507468 目录 part one 图像平滑处理 1原理 2代码 ...
- OpenCV腐蚀和膨胀Eroding and Dilating
OpenCV腐蚀和膨胀Eroding and Dilating 腐蚀和膨胀Eroding and Dilating 目标 形态运算 膨胀 侵蚀 代码 解释 腐蚀功能 膨胀功能 结果 腐蚀和膨胀Erod ...
- OpenCV之imgproc 模块. 图像处理(4)直方图均衡化 直方图计算 直方图对比 反向投影 模板匹配
直方图均衡化 目标 在这个教程中你将学到: 什么是图像的直方图和为什么图像的直方图很有用 用OpenCV函数 equalizeHist 对图像进行直方图均衡化 原理 图像的直方图是什么? 直方图是图像 ...
- OpenCV 【十九】图像金字塔/基本的阈值操作/实现自己的线性滤波器
目录 1.part one 图像金字塔 1.1原理 1.1.1图像金字塔 1.1.2高斯金字塔 1.2代码 1.3运行结果 2.part two 基本的阈值操作¶ 2.1原理 2.1.1阈值化的类型: ...
- OpenCV之imgproc 模块. 图像处理(5)在图像中寻找轮廓 计算物体的凸包 创建包围轮廓的矩形和圆形边界框 为轮廓创建可倾斜的边界框和椭圆 轮廓矩 多边形测试
在图像中寻找轮廓 目标 在这个教程中你将学到如何: 使用OpenCV函数 findContours 使用OpenCV函数 drawContours 原理 例程 教程的代码在下面给出. 你也可以从 这里 ...
- OpenCV之imgproc 模块. 图像处理(3)霍夫线变换 霍夫圆变换 Remapping 重映射 仿射变换
霍夫线变换 目标 在这个部分您将学习到: 使用OpenCV的以下函数 HoughLines 和 HoughLinesP 来检测图像中的直线. 原理 Note 以下原理的说明来自书籍 学习OpenCV ...
- 数字图像处理(十)腐蚀和膨胀
文章目录 前言 一.腐蚀 1.概念 2.算法的具体步骤 3.举例 4.python代码 二.膨胀 1.概念 2.算法步骤 3.举例 4.C++代码 5. 结果展示 参考资料 前言 二值图像中一类主 ...
- opencv 膨胀_【3】OpenCV图像处理模块(5)更多的形态学变换(开、闭、形态梯度、顶帽、黑帽)...
形态学变换有多种类型,上一节展示了最基本的腐蚀和膨胀.本节使用OpenCV提供的 cv::morphologyEx()函数实现多种形态学变换,如开运算.闭运算.形态学梯度.顶帽变换.黑帽变换等. 理论 ...
- 【OpenCV入门教程之十一】 形态学图像处理(二):开运算、闭运算、形态学梯度、顶帽、黑帽合辑
本系列文章由@浅墨_毛星云 出品,转载请注明出处. 文章链接: http://blog.csdn.net/poem_qianmo/article/details/23184547 作者:毛星云(浅墨) ...
最新文章
- java简单线程池实例代码
- Protocol Buffer C++应用实例
- 尝试在centos5下运行phantomjs2
- java控制器接收请求参数_SpringMVC之接收请求参数和页面传参
- logback 的 access 模块
- 博客园修改页面显示样式css
- dubbo源码解析(一)
- java线程卡住排查_基于 Java 线程栈 排查问题
- __attribute__((section(name))) study again
- 蔚来2018年平均每天亏掉2641万,车辆交付预期环比腰斩
- mesh 协调器 路由器_北京无线路由收发器C32MESH
- 内容营销——网络营销的杀手级武器
- 【TDA4系列】Linux SDK安装与交叉编译测试,以及刷写SD卡
- 微服务网关SpringCloud Gateway
- video.js插件的使用
- 6264.斐波那契(快速幂)
- html5/css登录注册网页模板
- 切图具体需要切什么内容_APP切图详细规范终极指南
- Mac下安装Mysql 记录过程
- windows记事本特别注意
热门文章
- 2019区块链行业指南
- 有赞搜索引擎实践(算法篇)
- AR、美颜、机器人:计算机视觉库几乎无所不在
- 白话Elasticsearch72_利用HDFS备份与恢复ES生产集群的数据
- 从零开始的51单片机——(1)点亮LED
- 织梦网站被黑客生成html,dedecms网站被挂马怎么处理
- Mysql的左外连接丶右外连接与内连接的区别
- python 选择排序算法
- celery 可视化_在Flask中使用Celery进行多任务分布执行
- centos 调整home分区xfs_Linux中对lvm逻辑卷分区大小的调整教程(针对xfs与ext4不同文件系统)...