1. 图像格式和通道
1.1 图像格式
图像格式是指计算机存储图像的格式。OpenCV目前支持的图像格式包括Windows位图文件BMP、DIB,JPEG文件JPEG、JPG、JPE,便携式网络图形文件PNG等。
①. BMP
BMP(全称Bitmap,位图)是Windows操作系统中的标准图像文件格式,可以分成两类——设备相关位图(Device Dependent Bitmap,DDB)和设备无关位图(Device Inpendent Bitmap,DIB),使用非常广。BMP采用位映射存储格式,除了图像深度可选以外,不采用其他任何压缩,因此,BMP文件所占用的空间很大。此种存储格式支持RGB、灰度、索引、位图等色彩模式,但不支持alpha通道,是Windows操作系统环境下最不容易出错的文件保存格式之一。
②. JPEG
JPEG格式是最常见的,也是用得最多的图像格式之一,被大多数的图像处理软件所支持。JPEG格式的图像还被广泛应用于网页的制作。该格式还支持CMYK、RGB和灰度色彩模式,但不支持alpha通道。其优点为兼容性好、传输速度快、占用内存小。
③. PNG
PNG(全称Portable Network Graphics),便携式网络图形是一种无损压缩的位图格式。其试图代替GIF和TIFF文件格式,同时增加一些GIF文件格式所不具备的特性。PNG格式具有压缩比高、生成文件体积小的特点。
1.2 通道分离和合并
在自然界中,颜色本身非常容易受到光照的影响,RGB图像像素值变化很大,而梯度信号能提供更本质的信息。三通道转为单通道后,运算量大大减少,便于后续处理。图像处理不一定需要对R、G、B 3个分量都进行处理。在图像处理中,尤其是处理多通道图像时,有时需要对各个通道进行分离,分别处理;有时还需要对分离处理后的各个通道进行合并,重新合并成一幅多通道的图像。 在OpenCV中,实现图像通道的分离与合并的函数分别为cv::split()和cv::merge()。
1.2.1 通道分离
split()函数用于将一个多通道数组分离成几个单通道数组。其C++版本的两个定义,分别如下。
void split(const Mat&src, Mat*mvbegin)
void split(InputArray m,OutputArrayOfArrays mv)
参数解析如下:
const Mat & 类型的原图像src或者InputArray 类型的m:表示待分离图像的多通道数组。
Mat*mvbegin或者OutputArrayOfArrays mv:表示分离后图像的Mat数组首地址,或者OutputArrayOfArrays类型的mv,即一个vector <Mat>对象。
举例:
// 1.分离vector<Mat> channels; //通道数组Mat imageBlueChannel;Mat imageGreenChannel;Mat imageRedChannel;Mat srcImage = imread("C:/Users/27844/Desktop/icon.jpg", 1);//flag=1,载入三通道图像if (!srcImage.data) {std::cout <<"图像读入失败!" << std::endl;exit(1);}//把一个三通道图像转化为3个单通道图像split(srcImage, channels);imageBlueChannel = channels.at(0);imageGreenChannel = channels.at(1);imageRedChannel = channels.at(2);//分别显示分离的单通道图像imshow("源图像", srcImage);imshow("<1>蓝色通道图像", imageBlueChannel);imshow("<2>绿色通道图像",imageGreenChannel);imshow("<3>红色通道图像",imageRedChannel);waitKey(0);
运行结果:
1.2.1 通道合并
merge()函数是split()函数的逆向操作——将多个数组合并成一个多通道的数组。它通过组合一些给定的单通道数组,将这些孤立的单通道数组合并成一个多通道数组,从而创建出一个由多个单通道阵列组成的多通道阵列。它有如下两个基于C++的多态函数定义。
void merge(const Mat*mv,size_tcount,OutputArray dst)
void merge(InputArrayOfArrays mv,OutputArray dst)
参数解析如下:
- 第一个参数mv:图像矩阵数组,表示需要被合并的输入矩阵或vector容器的阵列,这个mv参数中所有的矩阵必须有着一样的尺寸和深度。
- 第二个参数count:需要合并矩阵的个数,当mv为空数组时,代表输入矩阵的个数,这个参数显然必须大于1。
- 第三个参数dst:输出矩阵或图像,和mv[0]拥有一样的尺寸和深度,并且通道的数量是矩阵列中通道的总数。
举例:
// 2.合并//对拆分的数据进行合并Mat mergeImage;merge(channels, mergeImage);//合并imshow("rgb", mergeImage);waitKey(0);
2. 点运算
点运算是将输入图像映射为输出图像的方法,输出图像每个像素点的灰度值仅由对应的输入像素点的值决定。
点运算常用于改变图像的灰度范围及分布。因作用性质,点运算有时也被称为对比度增强、对比度拉伸或灰度变换。点运算实际上是灰度到灰度的映射过程,设输入图像为A( x , y ),输出图像为B( x , y ),则点运算可表示为B( x , y )= f [A (x , y )],即点运算完全由灰度映射函数 s = f ( r )决定, r 和 s 分别为A和B图像在( x , y )的灰度, f ()为变换函数。点运算不会改变图像内像素点之间的空间关系,点运算包括图像灰度变换、直方图处理、伪彩色处理等技术。
2.1 像素点亮度操作
对图像中每个像素加上(或减去)一个常数。设像素亮度为 v , b 是亮度常数,公式为 v = v + b ,如果 b 为正数,则像素亮度增强,如果 b 为负数,则像素亮度减弱。
在图像处理方面,无论加、减、乘、除,结果都可能会超出一个像素灰度值的范围(0~255),其中saturate_cast()函数的作用是当运算完之后,结果为负则转为0,结果超出255则转为255。下面的代码对像素亮度增加5:
for (int i = 0; i < Row; i++)
{for (int j = 0; j < Col; j++){for( int c = 0; c < 3; c++ )Scr.at<Vec3b>(i, j)[c]= saturate_cast<uchar> (Scr.at<Vec3b>(i, j)[c]+5 );}
}
2.2 图像卷积操作(也叫掩膜操作)
2.2.1 卷积
图像卷积是对图像进行处理时最常用的方法之一,如去噪、滤波、边缘提取等都要用到卷积函数,其目的之一是使各个目标之间的差距变得更大。卷积在数字图像处理中常见的应用为锐化(突出图像上物体的边缘、轮廓,或某些线性目标要素的特征)和边缘提取。数字图像是一个二维的离散信号,对图像做卷积操作其实就是利用卷积核(卷积模板)在图像上滑动,将图像点上的像素灰度值与对应的卷积核上的数值相乘,然后将所有相乘后的值相加作为卷积核中间像素对应的图像上像素的灰度值,并让卷积核最终滑动完所有像素的过程。
卷积值的计算过程如下:
(1)将核的锚点(中心点)放在要计算的像素上,卷积核剩余的部分对应在图像相应的像素上。
(2)将卷积核中的系数和图像中相应的像素值相乘,并求和。
(3)将最终结果赋值给锚点对应的像素。
(4)通过将卷积核在整张图像中滑动,重复以上计算过程,直到处理完所有的像素。
卷积核的选择有以下一些规则:
- 卷积核的大小一般是奇数,这样的话它就是按照中间的像素点中心对称的,所以卷积核的尺寸一般都是3x3、5x5或者7x7。有中心,也就有了半径,例如5x5大小的卷积核的半径就是2。
- 卷积核所有的元素之和一般应等于1,这是为了维持源图像的能量(亮度)守恒。
- 如果矩阵中所有元素之和大于1,那么卷积后的图像就会比源图像更亮;反之,如果小于1,那么得到的图像就会变暗。如果和为0,图像虽然不会变黑,但也会非常暗。
- 卷积后的结果可能会出现负数或者大于255的数值。对于这种情况,将值直接截断到0到255之间即可。对于负数,也可以取绝对值。
2.2.2 卷积操作
OpenCV中,使用filter2D()函数完成图像卷积操作。其函数定义如下。
CV_EXPORTS_W void filter2D(
InputArray src,
OutputArray dst,
int ddepth,
InputArray kernel,
Point anchor=Point(-1,-1),
double delta=0,
int borderType=BORDER_DEFAULT );
参数解析如下:
- Input Array src:输入图像。
- Output Array dst:输出图像。输出图像和输入图像具有相同的尺寸与通道数量。
- int ddepth:目标图像深度。当输入值为-1时,目标图像深度和源图像深度保持一致。
- Input Array kernel:卷积核,是一个矩阵。
- Point anchor:内核的基准点(anchor)。其默认值为(-1,-1),说明基准点位于核的中心位置。基准点即核中与进行处理的像素点重合的点。
- double delta:在储存目标图像前可选的添加到像素的值,默认值为0。
- int border Type:像素向外逼近的方法,默认值是BORDER_DEFAULT。
// 1.第一种方式Mat src, dst;src = imread("C:/Users/27844/Desktop/icon.jpg");if (!src.data) {cout << "open picture error!!" << endl;}CV_Assert(src.depth() == CV_8U);//图像深度imshow("src", src);//显示源图像int cols = (src.cols - 1) * src.channels();//由于最外围的一圈像素点没办法进行图像掩模,所以减1int rows = src.rows;//实际行数int offsets = src.channels();//通道数dst = Mat(src.size(), src.type());//大小、类型与源图像等同for (int row = 1; row < (rows - 1); row++) {const uchar*pre = src.ptr<uchar>(row - 1);//上一行const uchar* cur = src.ptr<uchar>(row);//当前行const uchar* next = src.ptr<uchar>(row + 1);//下一行uchar* output = dst.ptr<uchar>(row);for (int col = offsets; col < cols; col++) {output[col] = saturate_cast<uchar>(5 * cur[col] - (cur[col - offsets] + cur[col + offsets] + pre[col] + next[col]));}}imshow("dst", dst);
// 2.第二种方式:使用filter2D()函数Mat image = imread("C:/Users/27844/Desktop/icon.jpg");imshow("begin",image);// 定义一个卷积核Mat kernel = (Mat_<double>(3,3)<<-1,0,1,-2,0,2,-1,0,1);Mat dstImage; //结果图filter2D(image,dstImage,image.depth(),kernel);imshow("end",dstImage);waitKey(0);
代码中通过Mat_类代替Mat类来简化代码,Mat_类与Mat类的使用方法类似,具体如下。
Mat_<float> cMatrix = Mat::eye(3, 3, CV_32F);
∥"3,3"表示3行3列,且对角为1的矩阵;" CV_32F"表示0~1.0之间的32位浮点数
cMatrix(0, 0) = 2.5;
cout << cMatrix(0,0) << endl;//输出结果2.5
如果是定义为Mat类,则需要进行如下操作。
Mat testM = Mat::eye(3, 3, CV_32F);
testM.at<float>(0, 0) = 2.5;
cout << testM.at<float>(0,0) << endl;//输出结果2.5
不同的卷积操作只需改变卷积核即刻,下面是几个常见的卷积核:
- 平滑均值滤波卷积核。平滑均值滤波卷积核如图所示。该卷积核取9个值的平均值代替中间像素值,所以能起到平滑的效果。
- 高斯平滑滤波卷积核。高斯平滑滤波卷积核如图所示。高斯平滑水平和垂直方向呈现高斯分布,更突出了中心点在像素平滑后的权重,相比均值滤波有着更好的平滑效果。
- 图像锐化卷积核。图像锐化卷积核如图所示。该卷积利用了图像中的边缘信息有着比周围像素更高的对比度这一特点,经过卷积之后这种对比度进一步增强,从而使图像显得棱角分明、画面清晰,起到锐化图像的效果。
2.3 图像反转
反转变换适用于增强嵌入在图像暗色区域的白色或灰色细节,特别是当黑色面积较大时。灰度级范围为[0, L −1]的图像反转可定义为 s = L −1− r ,其中, s 为反转目标图像某点的灰度值, r 为源图像对应点的灰度值, L 为灰度级数(例:位深度为8位的是0~255)。
void function3()
{// 图像反转// 1.第一种方式Mat src = imread("C:/Users/27844/Desktop/icon.jpg");if(src.empty()){return;}int height = src.rows; //Mat对象行数int width = src.cols * src.channels();//Mat对象的列数乘通道数//显示图像imshow("src",src);// 将图像反转for(int i =0;i<height;++i){for(int j=0;j<width;++j){src.at<uchar>(i,j) = 255 - src.at<uchar>(i,j); //每一个像素反转}}//再次显示图像imshow("dst",src);// 2.第二种方式:bitwise_not()函数Mat dst;bitwise_not(src,dst);imshow("dst2",dst);waitKey(0);}
运行结果:
2.4 对数变换(对每个像素点的像素值进行对数运算)
对数变换可以拉伸范围较窄的低灰度值,同时压缩范围较宽的高灰度值;可以用来扩展图像中的暗像素值,同时压缩亮像素值。对数变换的一般表达式如下。
S = c log(1+ r ) (2-1)
其中,c为常数,1+ r 可以使函数向左移一个单位,得到的 S 均大于0。
假设 r ≥0,进行对数变换时窄带低灰度输入图像值将映射为宽带输出值。
//对数变换举例//定义对数变换模块
void log_transfor(Mat& image, Mat& result)
{result = image.clone();int rows = image.rows,cols = image.cols;for (int i = 0; i < rows;i++){for (int j = 0;j < cols; j++){for (int k = 0;k < 3;k++){// 对数变换result.at<Vec3b>(i, j)[k] = 31 * log2(1 + image.at<Vec3b>(i, j)[k]);}}}
}
void function4()
{// 1.对数变换Mat image = imread("C:/Users/27844/Desktop/icon.jpg");Mat result;log_transfor(image,result);imshow("input",image);imshow("output",result);waitKey(0);}
2.5 幂律变换
幂律变换又叫伽马变换,和对数变换的原理差不多,只是参数多了一个,可变宽带的输入像素值范围可选,但是把低值带拉伸还是把高值带拉伸则取决于伽马的设定。
幂律变换的基本形式如下。
s = cr γ (2-2)
其中, s 和 r 分别表示处理前后的像素值, c 和 γ 为正常数。
- 幂律变换通过幂次变换曲线中的 γ 值把输入的窄带值映射到宽带输出值。
- 当 γ >1时,把输入的窄带暗值映射到宽带输出亮值,提高图像中亮区域的对比度。
- 当 γ <1时,把输入高值映射为宽带,图像整体灰度值增大,提高图像暗区域中的对比度。
2.6 线性变换
线性变换可分为线性变换和线性分段变换。线性变换是灰度变换的一种,图像的灰度变换通过建立灰度映射来调整源图像的灰度,从而达到图像增强的目的。
一般成像系统只具有一定的亮度范围,亮度的最大值与最小值之比称为对比度。由于形成图像的系统亮度有限,图像常出现对比度不足的问题,使人眼观看图像时获得的视觉效果很差,通过变换法可以大大改善图像的视觉效果。线性变换公式可以表示为 g ( x , y ) = k × f ( x , y ) + b 。其中 g ( x , y )表示变换后的目标像素值, f ( x , y )表示原图像素值, k 表示斜率, b 表示截距。 k 的取值和变换作用有以下4种情况。
- 当 k >1时,可用于增加图像的对比度。图像的像素值在变换后全部增大,整体显示效果被增强。
- 当 k = 1时,常用于调节图像亮度。
- 当0< k <1时,效果与 k >1时相反,图像的对比度和整体效果都被削弱。
- 当 k <0时,源图像较亮的区域变暗,而较暗的区域会变亮。此时可以使函数中的 k =−1、 b =255让图像实现反色效果。
OpenCV中使用convertScaleAbs()函数与convertTo()函数对图像进行线性变换,其中convertScaleAbs()函数定义如下。
void convertScaleAbs(InputArray src, OutputArray dst, double alpha = 1, double beta = 0);
参数解析如下:
- InputArray src:输入图像。
- OutputArray dst:输出图像。
- double alpha:缩放因子。
- double beta :缩放后的固定值,偏移量。
注意,src和dst的类型与通道数相同。
对于convertTo()函数,OpenCV主要支持单通道和三通道的图像,并且此时要求其深度为8位和16位无符号(即CV_16U),而其他一些数据类型是不支持的,例如float型等。如果Mat类型数据的深度和通道数不满足上面的要求,则需要使用convertTo()函数和cvtColor()函数来进行转换。convertTo()函数负责转换数据类型不同的Mat对象,即可以将类似float型的Mat对象转换为imwrite()函数能够接受的类型。convertTo()函数定义如下。
src.convertTo(dst, type, scale, shift);
参数解析如下:
- dst:目的矩阵。
- type:需要输出的矩阵类型,或者是输出矩阵的深度,如果是负值(常用−1),则输出矩阵和输入矩阵类型相同。
- scale和shift:缩放参数,也可以写为alpha和beta。
线性变换分为 全域线性变换 和 分段线性变换。
void function5()
{// 线性变换// 1.使用convertScaleAbs()函数Mat src = imread("C:/Users/27844/Desktop/icon.jpg");Mat dst;convertScaleAbs(src,dst,1.5,10);imshow("src",src);imshow("dst",dst);// 2.使用convertTo()函数Mat dst2;src.convertTo(dst2,1,20);imshow("dst2",dst2);waitKey(0);}
2.7 图像灰度化
我们平时看到的图像大部分都是彩色图像,在计算机中用RGB(红绿蓝)颜色模式显示图像,RGB 3个分量的取值范围都是0~255,从光学原理上按照不同的亮度比例进行颜色调配。如RGB(255,255,0)将红色与酸橙色(亮绿色)进行调配表示为黄色,RGB(128,0,128)将栗红色(暗红色)与海军蓝(深蓝色)混合为紫色。在RGB模型中,在R=G=B时,彩色会在视觉上呈现出一种灰色,其中R、G、B的值叫灰度值,灰度范围为0(黑色)到255(白色)。灰度值是形容黑白图像的,亮度值是形容彩色图像的。如果需要把彩色图像转换成黑白图像,那么亮度值就会作为转换后的黑白图像的灰度值。
在图像处理时,如果采用RGB模式进行计算,那么需要分别对RGB的3个分量进行处理,而且实际上RGB并不能反映图像的形态特征,只是从光学原理上进行颜色的调配。所以将彩色照片转换为灰度图进行统一处理,这个操作称为图像灰度化。
图像灰度化通常有两种方法:平均值法 和 加权平均法。
平均值法直接将彩色图像中的3个分量亮度求平均得到灰度值,公式如下:
而由于人眼对绿色的敏感度最高,对蓝色的敏感度最低,因此,对RGB 3个分量进行加权平均能得到更合理的灰度图,公式如下:
举例 :
void function6()
{// 图像灰度化Mat src = imread("C:/Users/27844/Desktop/icon.jpg");imshow("src",src);//平均值法,与彩色图像大小相同,通道数为1Mat gray1(src.rows,src.cols,CV_8UC1);for(int i=0;i<src.rows;i++){for(int j=0;j<src.cols;j++){gray1.at<uchar>(i,j) = (src.at<uchar>(i,j*3) + src.at<uchar>(i,j*3 + 1) + src.at<uchar>(i,j*3 + 2)) / 3;}}//加权平均值法Mat gray2(src.rows,src.cols,CV_8UC1);for(int i=0;i<src.rows;i++){for(int j=0;j<src.cols;j++){gray2.at<uchar>(i,j) = (src.at<uchar>(i,j*3)*11 + src.at<uchar>(i,j*3 + 1)*59 + src.at<uchar>(i,j*3 + 2)*30) / 100;}}imshow("gray1",gray1);imshow("gray2",gray2);waitKey(0);
}
运行结果: