初稿完成于2020.1.28
内容包括图像在计算机中的存储、形态学滤波操作、画图操作三个部分。
图像在我们眼中是一幅一幅色彩斑斓的图案,那在计算机中是什么呢?
在计算机中存储的图像并不是那种直观的、让人能够感知到的色块,而是数字——将图像的每一个像素点以数字的方式来表示(前提是确保数字能包含你所需要的像素点的所有特征),然后再将这些像素点“组装”成一个矩阵阵列,把这个矩阵(在OpenCV里叫做Mat类)存储在计算机中,这样就能够由一个个的像素点构成一幅完整的图像。
嗯,于是我们就需要了解一下用多少数字、多大的数字才能够准确地描述像素点的特征~~~
多少数字在图像处理中称作图像的通道。比如,我们通常在说一幅图像时(尤其是在P图时),会提到图像的对比度、亮度、色调、饱和度等特征,这些特征的数目多少就称为一幅图像的通道。
当然,在计算机来描述一幅图像时所使用的特征与我们平时所熟悉的并不一样,这在后面会举例加以介绍。
描述图像的特征时所使用的数字的大小称作图像的**“深度”**。要注意的是,这里的“深度”按bit的大小来定义,也就是说,1位深度的图像中的各个通道的数字大小为 2 1 2^1 21,8位深度的图像中则为 2 8 2^8 28,以此类推。因为C++的类型存储多以0开始计数而不是1,在这里也是如此,1位深度为[0,1]区间,8位深度为[0,255]区间。
下面我们来看看不同的图像在计算机中是通过提取哪些信息来存储的。首先是黑白图像——
黑白图像,又称二值图像,是个单通道1位深度的图像(只需要一个量,0~1两个数字即可存储像素点中的所有信息)。其中,0代表黑色,1代表白色。
单通道8位深度。(只需要一个取值范围为[0,255]的量即可描述像素点的特征,即表示灰度值)其中,0代表黑色,255代表白色,中间值则为深浅不一的灰色。
三通道8位深度。用红(R)、绿(G)、蓝(B)三种颜色来描述彩色图像的像素点中的特征,三种颜色以一定比例混合则可得到色彩斑斓的图案。有的时候也可以在RGB颜色空间中加上一个透明度(A)的通道,可称为RGBA颜色空间,为四通道的。
三通道8位深度。为什么有了RGB颜色空间可以描述彩色图像,还需要其它描述彩色图像的颜色空间呢?这是因为RGB颜色空间在进行图像处理时不易于提取图像中的特征(尤其是在颜色识别中),因此我们需要引入其它的颜色空间,这里的HSV空间就是常用的一种(也有其它的,可以自行去了解,百度谷歌)。
其中,HSV三者分别代表色调、饱和度、亮度。下面是该颜色空间的模型:
说到类,就不得不说C++。“类”是C++的一个不同于C语言的特性,在初学的时候我们不需要去了解其中的具体内容是什么,我们可以把“类”简单粗暴地理解为一个类似结构体的东西,里面封装了很多我们可以去使用的变量、或者是函数。
而Mat类则是OpenCV中用于存储图像矩阵的一种变量类型,直接把它当成变量类型来看就好了。我们可以像定义整型、浮点数类型一样来定义一个Mat变量,用于存储矩阵。
而Mat类中包含的一些常用的成员变量有:data(Mat对象中的头指针);dims(Mat所代表的矩阵的维度);channels(图像的通道);depth(图像的深度)等。
下面是创建一个300*300的白色矩阵的代码(使用的是3通道8位深度的颜色空间,Scalar类在part2中会讲解):
Mat image_white(300, 300, CV_8UC3, Scalar(255, 255, 255));
//其中所填的参数分别代表:矩阵的行、列、类型、颜色
//此代码等价于:
Mat Image;
Image.create(300, 300, CV_8UC3, Scalar(255, 255, 255));
要注意的是,若直接用一个Mat变量给另一个Mat变量赋值,则会将Mat变量的所有成员变量一并传递。也就是说,头指针也会一并传递(头指针就是Mat变量在栈中所占的首尾地址,可以用数组的头指针来类比想象)——这就意味着,这是个传地址的操作,当对其中一个Mat变量进行操作时,另一个也会跟着改变。因此,当我们想要创建一个与原图像完全相同,但却不希望改变原图像时,就需要用到Mat类中的一些成员函数(成员函数是封装好了在类中的一些函数),其使用方法和C语言中的函数调用有所不同,是在该类的变量后加上“.”再加上函数名来实现(和C语言中访问结构体成员的运算符一样)。可参考上一小段代码中的create成员函数的调用方法。
//Mat类的传值
Mat srcImage,dstImage;
dstImage=srcImage.clone();
//等价于dstImage=srcImage.copyTo();
//Mat类的传地址
dstImage=srcImage;
imshow
函数就是图像的显示(好比C语言中的printf
,没有输出的代码是没有灵魂的QAQ),其中视频流的读取用的是VideoCapture
类型的变量,其使用方法会在part2中讲解;而imread
与imwrite
则是图像的输入输出到文件。
imshow函数
用于显示图像
imshow("const string&winname",//显示图像的窗口标识名称
srcImage); //所显示的Mat型变量的图像
呐,用的时候就像这样,就会显示以“源图像”为窗口名的srcImage
图像:
imshow("源图像", srcImage);
但要注意的是,imshow
函数在调用时,窗口名一样的会“后者覆盖前者”,也就是说,当你连续调用两次上述代码时,不会出现两个同名窗口。要记住不会出现同名窗口啊!!!
imread()函数
载入图像,用于读取文件中的图片到Mat对象中。函数原型:
Mat imread(
const string& filename,//需要载入的图片的路径名
int flags=1); //指定加载图像的颜色类型
flags的取值范围 | 作用 |
---|---|
flags>0 | 返回一个3通道的彩色图像 |
flags=0 | 返回灰度图像 |
flags<0 | 返回包含Alpha通道的加载的图像 |
示例(从当前目录读取):
Mat srcImage = imread("SAST.jpg",0);//读入一个灰度图像到srcImage中
imwrite()函数
输出Mat类型的图像到文件中。函数原型:
bool imwrite(
const string& filename,//想要输出的图片路径
InputArray img, //输出的图像
const vector<int>& params=vector<int>());//一般不用填
示例(输出到当前路径):
//假设之前srcImage以及有了值
imwrite("1.png",srcImage);
createTrackbar()函数
用于创建一个可以调整数值的滑动条。这里要注意的是,**不要调用回调函数啊!!!不要用回调函数!!!**不然的话你会发现卡的一批!!!直接将value设置为全局变量就好了!!!代码示例参考随附代码的part3啊!!!
int createTrackbar(
conststring& trackbarname, //轨迹条的名字
conststring& winname, //窗口的名字
int* value, //滑块的位置
int count, //滑块可以达到的最大位置(滑块最小的位置的值始终为0)
TrackbarCallback onChange=0,//指向回调函数的指针
void* userdata=0); //用户传给回调函数的数据
还有一些常用函数,比如用于创建窗口的namedWindow
函数(在使用imshow
的时候会自动创建一个窗口,但如果想要在显示之前就有那个窗口,比如使用滑动条的时候,就需要用到这个函数);用于确定滑动条位置的getTrackbarPos
函数(但是如果把value设为全局变量就不需要使用函数获取位置了);还有一些关于鼠标操作的函数等等,这里不一一介绍,感兴趣的自行去了解。
滤波中有关邻域算子、掩膜、锚点等重要概念将会在part3中讲解。代码参见随附的part1.c文件。
腐蚀是对图像中的高亮(白色)区域进行腐蚀,即求局部区域最小值的操作。下图的示例是用3*3的算子对二值图像进行腐蚀操作,求取对应位置的8邻域的最小值并输出:
erode函数
用于图像的腐蚀
void erode(
src, //源图像
dst, //目标图像,需要和原图像有一样的尺寸和类型
kernel, //腐蚀操作的核,当为NULL时,表示的是参考点位于中心3阶的核
Point anchor=Point(-1,-1), //下面的不填就完事了
int iterations=1,
int borderType= BORDER_CONSTANT ,
const Scalar& borderValue =morphologyDefaultBorderValue());
膨胀是对图像中的高亮(白色)区域进行膨胀,即求局部区域最大值的操作
dilate函数
用于图像的膨胀
void dilate(
src, //源图像
dst, //目标图像,需要和原图像有一样的尺寸和类型
kernel, //膨胀操作的核,当为NULL时,表示的是参考点位于中心3阶的核
Point anchor=Point(-1,-1),//下面的不填就完事了
int iterations=1,
int borderType = BORDER_CONSTANT,
const Scalar& borderValue= morphologyDefaultBorderValue());
先腐蚀后膨胀的过程。可以用来消除小物体、平滑物体边界的同时不明显改变其体积。
先膨胀后腐蚀的过程,能够排除小型黑洞(黑色区域)
膨胀图与腐蚀图之差,用于突出图像边缘
原图像与“开运算”结果图之差,得到比原图轮廓周围的区域更明亮的区域,用 来分离比邻近点亮一些的斑块
“闭运算”的结果图与原图像之差,突出了比原图轮廓周围的区域更暗的区域,用来 分离比邻近点暗一些的斑块。
用于返回指定形状和尺寸的结构元素
getStructuringElement(
int shape, //内核的形状
Size esize, //内核的尺寸,写成Size(n,n)形式,n越大腐蚀效果越明显
Point anchor=Point(-1,-1));//锚点的位置
//内核的形状包括:
MORPH_RECT //矩形
MORPH_CROSS //交叉型
MORPH_ELLIPSE//椭圆形
用于各种形态学滤波操作
void morphologyEx(
src, //源图像
dst, //目标图像
int op, //形态学运算的类型
kernel, //形态学运算的内核,若为NULL,表示使用参考点位于中心3阶的核
Point anchor=Point(-1,-1), //锚点位置
int iterations=1, //迭代使用函数的次数
int borderType=BORDER_CONSTANT,//推断图像外部像素的某种边界模式
const Scalar& borderValue =morphologyDefaultBorderValue());//用于单独操作每一个通道之类
形态学运算类型op的取值:
符号 | 形态学运算类型 |
---|---|
MORPH_OPEN(or 0) | 开运算 |
MORPH_CLOSE(or 1) | 闭运算 |
MORPH_GRADIENT(or 2) | 形态学梯度 |
MORPH_TOPHAT(or 3) | 顶帽 |
MORPH_BLACKHAT(or 4) | 黑帽 |
MORPH_ERODE(or 5) | 腐蚀 |
MORPH_DILATE(or 6) | 膨胀 |
调用代码示例:
//提供形态学运算用的算子
Mat element = getStructuringElement(MORPH_RECT, Size(3, 3));
//形态学滤波操作
morphologyEx(srcImage, dstImage, 1, element);
在图像上进行画图操作,从底层来看,就是将图像中需要画的像素点依次赋值成想要的颜色,从而实现在视觉上的“画图”的效果。而OpenCV中我们不需要自己从底层开始操作(就是把图像中像素点依次提取出来再重新赋值),其中有现成的封装好了的画图函数可以供我们调用。
下面列举一些常用的函数,其它的画图操作的函数可自行上网查阅:
用于画直线。下面是函数原型:
void line(Arr* img, //图像
Point pt1, //线段的第一个端点
Point pt2, //线段的第二个端点
Scalar color, //线段的颜色
int thickness=1, //线段的粗细
int line_type=8, //线段的类型
int shift=0 );//坐标点的小数点位数
//线段类型:
8 //(8邻接)连接线
4 //(4邻接)连接线
CV_AA //线条
用于绘制矩形。下面是函数原型:
void rectangle(
Arr* img,//图像
Point pt1,//矩形的一个顶点
Point pt2,//矩形对角线上的另一个顶点
Scalar color,//线条颜色
int thickness=1,//线条粗细,取负值时,则填充
int line_type=8,
int shift=0 );
用于画圆,下面是函数原型:
void circle(
InputOutputArray img, //表示输入的图像
Point center, //圆心坐标
int radius, //圆的半径
const Scalar& color, // Scalar类型,表示圆的颜色
int thickness = 1, //线的宽度
int lineType = LINE_8, //线的类型,默认为8联通型
int shift = 0);
下面是两个在OpenCV提供的函数的基础上封装好的更便于使用的函数:
//画椭圆
void DrawEllipse(Mat Image,int window_size, double angle)
{
ellipse(Image, //所操作的图像
Point(window_size / 2, window_size / 2),//中心点
Size(window_size / 4, window_size / 16),//大小位于此内
angle,//旋转角度
0, 360,//扩展弧度为【0,360】
Scalar(200, 19, 12),//颜色
2,//线宽
8);//线条类型为8连通线型
}
//绘制实心圆
void DrawFilledCircle(Mat Image, int window_size, Point center)
{
circle(Image,//所操作的图像
center,//中心点坐标
window_size / 32,//圆的半径
Scalar(178, 0, 159),//颜色
-1,//线宽为填充型
8);//线条类型为8连通线型
}
当然,在某些特定场景下,我们也需要对图像中的像素点单独进行一些操作,需要其中的一些信息。要注意的是,在进行图像处理的写代码的过程中,需要谨记,图像是以一个矩阵的形式在计算机中存储的,尽管OpenCV给我们封装好了很多完善的函数,但如果想要写好图像处理,仅仅会使用这些函数是远远不够的,我们需要了解这些函数的内部操作(甚至也可以对它们进行修改),这样才能够提高我们的代码质量。
并且,Mat变量是可以直接使用±运算符进行加减的,在某些特殊场合也可以加以使用。下节课会介绍如何访问Mat变量里的单个像素点的操作,以备使用。