OpenCV的机器视觉图像处理技术实现之Core模块
影像处理早期是从扫描的文件中识别出文字(OCR),后来才发展为手写识别、自拍修图等静态图像处理。机器人技术的应用从早期的组装自动化,到中期的生产质量监控,再到近期逐渐走进人群的应用,整个发展过程中都涉及图像处理的技术。因此,图像处理不仅会更加流行,而且会更加普及和接近大众。
Core 模块是OpenCV最基本的模块,因为所有OpenCV需要的数据结构与基本的绘图功能都在Core模块内,所以在项目中,Core也是第一个一定要具备的模块。这些基本数据结构与绘图功能,请参考OpenCV说明文档的网址http://docs.opencv.org/。本文将以程序示例来介绍其实际应用,因为任何图书都不可能完全介绍OpenCV的所有功能,所以读者要养成经常查看这些说明文档的习惯。只要单击网页内的“core. The Core Functionality”就可以查看Core模块。要查看其他模块,方法也是相同的。
2.1 显示图文件
因为OpenCV有太多模块,而每个模块又有许多函数,本书就以程序示例的方式来介绍。在程序中使用到的函数,我们才会给出说明,其他未说明的部分,请读者自行查阅OpenCV的说明文档。
至此,项目终于设置完成,现在我们开始编写程序。因为我们只是学习操作,所以先剪切并粘贴代码比较简单,其方式如下:
在右侧“Solution Explorer”中的“Source File”中单击鼠标右键,选择“Add”再选择“New Item”来加入代码,如图2-1所示。
图2-1
输入程序文件名称,这里使用与项目相同的名称,如图2-2所示。
图2-2
本小节所介绍的代码设置,与其他项目使用的方式都相同,后面所有的项目按照此设置,不再重复说明。
继续先前项目,设置完成有了程序文件之后,就粘贴复制的代码,如图2-3所示。
图2-3
粘贴程序后再单击选单中绿色的三角形按钮,如图2-4所示。
图2-4
Visual Studio会询问是否进行编译,单击“Yes”按钮开始执行,如图2-5所示。
执行本程序之前,我们应该先将本书所使用的图文件都复制到C:\images文件夹下。如果图文件位于不同的目录,请自行修改程序对应的文件夹。
最后程序结果显示,如图2-6所示。
图2-5
图2-6
本项目只有一个程序文件。如果项目有许多C++程序与include文件,请全部复制到这个项目内,如图2-7所示。
要注意,就此范例而言,文件应该复制到Display_Image/Display_Image文件夹下。
图2-7
复制完成后再分别将复制的文件加入项目内。加载Include文件的方法与加载C++程序文件的做法一样,只是Include文件要加载Solution Explorer的Header File,C++文件要加载Solution Explorer的Source File。
现在以加入C++程序文件来做说明。在“Solution Explorer”的“Source File”上单击鼠标右键,再依次单击“Add”与“Existing item...”,如图2-8所示。
图2-8
Visual Studio就会弹出窗口让你选择要加入的内容,如图2-9所示。
图2-9
这只是OpenCV的“Hello World”(第一个测试程序的意思),所以直接写入要处理的文件(本书范例全都是直接写入的状态)。读者也可以自行与MFC或C++/CLI整合来处理UI问题,这部分不是本书探讨的范畴。
原始文件都是以argv作为输入文件,可以改变文件名,或在项目属性的“Debugging”选项中设置,如图2-10所示。笔者认为直接写入程序并且印在书中,读者在阅读时则能看得清程序的结构。
图2-10
注意
因为Windows 7在复制文件路径时会自动变成反斜杠,所以这里继续沿用,如图2-11所示。为了避免C++程序编译错误,所以改成双反斜杠,如果使用正斜杠就不需要再使用双正斜杠了,如图2-12所示。
图2-11
图2-12
程序内容如下:
#include <opencv2/core/core.hpp>#include <opencv2/highgui/highgui.hpp>#include <iostream>using namespace cv;using namespace std;int main( int argc, char** argv ){
argv[1] = "C:\\images\\lena.jpg";
Mat image;
// 载入图文件
image = imread(argv[1], CV_LOAD_IMAGE_COLOR);
// 检查读取文件是否成功
if(! image.data )
{
cout << "无法打开或找不到图文件" << std::endl ;
return -1;
}
// 建立显示图文件窗口
namedWindow("Display window", CV_WINDOW_NORMAL);
// CV_WINDOW_FREERATIO 与 CV_WINDOW_KEEPRATIO
// CV_GUI_NORMAL 与 CV_GUI_EXPANDED
// 在窗口内显示图文件
imshow( "Display window", image );
// 窗口等待按键
waitKey(0);
return 0;}
程序说明
本书先假设读者已对C++有所接触认识,代码内容属于C++而不是OpenCV的范畴,在此不再赘述。
1.Mat image
Mat是OpenCV新定义的数据类型,类似传统的数据类型int、float或String。Mat这种数据类型表示图像,而图像都是二维数组。所以OpenCV就定义了处理图像的矩阵类别(Matrix),取英文的前3个字母Mat,就如同int取integer的前3个字母一样。
OpenCV常用的数据结构有以下几种。
Point:代表二维的点,用于图像的坐标点。
Point pt;
pt.x = 10;
pt.y = 8;
或是:
Point pt = Point(10, 8)
Scalar:代表4元素的向量(4-element vector),一般用于RGB颜色值。
Scalar(a, b, c),第4个参数如果用不到可以省略。
a代表蓝色值、b代表绿色值、c代表红色值,也就是Scalar(B,G,R)。
MAT类别一般运算的说明见表2-1。
表2-1
语 法 | 说 明 |
---|---|
double m[2][2] = {{1.0, 2.0}, {3.0, 4.0}}; | 由多维数组(array)产生2×2矩阵 |
Mat M(100, 100, CV_32FC2, Scalar(1, 3)); | 声明100×100的两个通道的矩阵,第一个通道值是1,第二个通道值是3 |
M.create(300, 300, CV_8UC(15)); | 产生300×300的15个通道的矩阵,之前变量M内的值将被删除 |
Mat ima(240, 320, CV_8U, Scalar(100)) | 产生240×320,单一通道(灰阶)的矩阵 |
int sizes[3] = {7, 8, 9}; | 产生三维矩阵,各维的维度值(dimension)分别是7、8和9,矩阵的初始值是0 |
Mat M = Mat::eye(7, 7, CV_32F); | 产生32位浮点(float)的7×7的矩阵 |
Mat M = Mat::zeros(7, 7, CV_54F); | 产生64位浮点(float)的7×7的矩阵,矩阵初始值是0 |
M.at<double>(i, j) | 取得矩阵M在第i行、第j列的值,该值的数据类型是double。 |
M.row(1) | 取得矩阵M在第1行的值 |
M.col(3) | 取得矩阵M在第3列的值 |
M.rowRange(1, 4) | 取得矩阵M在第1行到第4行的值 |
M.colRange(1, 4) | 取得矩阵M在第1列到第4列的值 |
M.rowRange(2, 5).colRange(1, 3) | 取得矩阵M在第2行到第5行及第1列到第3列的值 |
M.diag(); | 取得矩阵M的对角值 |
Mat M2 = M1.clone(); | 将M1复制给M2 |
Mat M2; M1.copyTo(M2); | 将M1复制给M2 |
Mat M1 = Mat::zeros(9, 3, CV_32FC3); | 声明3行的M2矩阵,其通道与M1相同(reshape的第一个参数0),3行的数组是reshape的第二个参数3 |
Mat M2 = M1.t(); | M2是M1的转置(transpose) |
Mat M2 = M1.inv(); | M2是M1的逆矩阵(inverse) |
Mat M3 = M1 * M2; | M3是M1乘M2(矩阵相乘) |
Scalar s; | M2是M1矩阵加上s颜色值(scalar) |
Mat image; | Image矩阵的高和宽 |
Mat是数据类型,就会被当成参数在函数之间传递。但是因为图文件可能非常庞大,为了节省内存与性能,OpenCV都是以传址的方式处理的,也就是共用一份内存。现在用下面的代码来说明这一点。
Mat A, C;
A = imread(argv[1], CV_LOAD_IMAGE_COLOR);
C = A;
Mat fun(A);
上面的程序声明了两个图像数据类型A和C,A的内容是读入的图文件,然后A又传递C(C = A),A再以参数传递给fun函数。如果fun函数对A做任何处理,可能改变了A,fun函数执行完C也会被改变,即使C不在fun函数的作用域之内。
不同数据类型名称如果共用内存的话,何时才会真的从内存消失?OpenCV是使用C++的引用计数(reference couting),所以我们在设计程序时不用考虑做内存管理,否则就要执行对象释放(Release)的操作,这就是OpenCV 2.0与 OpenCV 1.0的差别。
2.imread (const string& filename, int flags):读取图文件
filename:图文件名称
flags:读取的方式。本例是读取彩色的内容。
OpenCV可以读取的图文件种类如下。
BMP:Windows位图文件。
PBM、PGM、PPM:可移植图文件格式。
SR、RAS:Sun的图文件格式。
JPEG、JPG、JPE:JPEG图文件格式。
TIFF、TIF:TIFF图文件格式。
PNG:可移植网络图文件格式。
读取的方式有下列3种选项:可使用如下例程名称,或用数值表示。
CV_LOAD_IMAGE_UNCHAMGED:这是以图文件的原始文件形态(彩色或黑白)读取,使用数值是小于零的数值。
CV_LOAD_GRAYSCALE:这是以黑白方式读取,使用数值是等于零的数值。
CV_LOAD_COLOR:这是以彩色方式读取,使用数值是大于零的数值。
3.namedWindow (const string& winname, int flags):建立要显示图文件的窗口
winname:窗口名称。这也表示可以同时建立多个窗口,再以名称区别
flags有下列几种选择。
WINDOW_AUTOSIZE:这是窗口大小不可改变,会按照图文件的大小自动调整;如果使用CV_WINDOW_AUTOSIZE也可以达到同样效果,这是OpenCV 1.0的用法
WINDOW_NORMAL:这是窗口大小可以改变,但必须是使用Qt开发;有关使用C++与Qt的说明,请参考http://qt-project.org/wiki/OpenCV_with_Qt。
WINDOW_OPENGL:设置支持OpenGL时,就要使用此参数。
不过笔者现阶段测试失败了,但是在C:\OpenCV\sources\modules\highgui\src\window_ w32.cpp程序中查找OPENGL部分,系统的确是有支持OpenGL的功能,可能OpenCV在构建程序库时有参数设置遗漏了。 - CV_WINDOW_NORMAL:如果重新建立对 OpenGL 的支持,想要再次使用WINDOW_AUTOSIZE就会出错,此时需要使用此参数。这可以使窗口大小随意改变,放大不会导致图像失真,缩小也不会影响原图内容比例。
这个参数在没有支持OpenGL时也可以使用,该选项特别适用于窗口上有滑动杆,而滑动杆大于窗口宽度时,或图太小想放大窗口看清效果。
4.imshow(const string& winname, InputArray mat):显示窗口
winname:窗口名称,会显示在窗口的左上方,方便窗口之间的分辨或比较。
mat:图文件所声明的矩阵参数。
本程序示范显示图像窗口,当程序结束时窗口就会自动关闭。如果程序还有其他部分在使用中,但又想关闭窗口时,则可以使用destroyWindow()来关闭窗口,或使用destroyAllWindows()来关闭所有窗口。
5.waitKey():等待按键
这是等待用户按下任意键继续执行,否则程序会立刻结束,并且无法看到程序执行结果。其参数是以毫秒(ms)为单位的等待时间。如果参数值为零,则表示持续等待。
注意,这个等待功能只能用于imshow所显示的窗口,若没有任何使用imshow显示的窗口,那waitKey()将没有任何作用。此时可改为用传统的getchar()实现程序执行的暂停功能。
如果将鼠标光标在图像的窗口上移动,光标会自动变成十字形,这表示OpenCV在窗口处理上还可以处理鼠标。第11章的实际案例应用的图像采集就是鼠标光标的示例。
图像数据结构实例
学习OpenCV,除了函数之外,就是数据结构最重要了,所以我们再以程序范例来对此加以说明。不过一般图像矩阵是无法用 << 来显示内容的,本程序只是以读者看得懂的方式来说明。
#include "opencv2/core/core.hpp"#include <iostream>using namespace std;using namespace cv;int main(int, char**){
// 用构造函数建立数据
Mat M(2, 2, CV_8UC3, Scalar(0, 0, 255));
cout << "M = " << endl << " " << M << endl;
// 用create函数建立数据
M.create(2, 2, CV_8UC(2));
cout << "M = " << endl << " " << M << endl;
// 建立多维矩阵
int sz[3] = { 2, 2, 2 };
Mat L(3, sz, CV_8UC(1), Scalar::all(0));
// 无法用 << 运算符输出
// 用MATLAB风格的眼建立数据
Mat E = Mat::eye(3, 3, CV_64F);
cout << "E = " << endl << " " << E << endl;
// 数据都是1
Mat O = Mat::ones(2, 2, CV_32F);
cout << "O = " << endl << " " << O << endl;
// 数据都是0
Mat Z = Mat::zeros(2, 2, CV_8UC1);
cout << "Z = " << endl << " " << Z << endl;
// 建立3x3双精确度矩阵,值由<<输入
Mat C = (Mat_<double>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
cout << "C = " << endl << " " << C << << endl << endl;
// 复制第一行数据
Mat RowClone = C.row(1).clone();
cout << "RowClone = " << endl << " " << RowClone << endl;
// 以随机数值填入矩阵内
Mat R = Mat(3, 2, CV_8UC3);
randu(R, Scalar::all(0), Scalar::all(255));
// 展示各种输出格式选项
cout << "R (default) = " << endl << R << endl;
cout << "R (python) = " << endl << format(R, "python") << endl;
cout << "R (numpy) = " << endl << format(R, "numpy") << endl;
cout << "R (csv) = " << endl << format(R, "csv") << endl;
cout << "R (c) = " << endl << format(R, "C") << endl;
// 图像中二维的点
Point2f P(5, 1);
cout << "Point (2D) = " << P << endl << endl;
// 图像中三维的点
Point3f P3f(2, 6, 7);
cout << "Point (3D) = " << P3f << endl << endl;
vector<float> v;
v.push_back((float)CV_PI);
v.push_back(2);
v.push_back(3.01f);
cout << "浮点向量矩阵 = " << Mat(v) << endl << endl;
vector<Point2f> vPoints(5);
for (size_t i = 0; i < vPoints.size(); ++i)
vPoints[i] = Point2f((float)(i * 5), (float)(i % 7));
cout << "二维图点向量 = " << vPoints << endl;
getchar();
return 0;}
执行结果如图2-13所示。
图2-13
下雪特效
下雪特效的代码如下:
#include <opencv2/core/core.hpp>#include <opencv2/highgui/highgui.hpp>#include <iostream>using namespace cv;using namespace std;int main(int argc, char** argv){
argv[1] = "C:\\images\\lake.jpg";
Mat image;
// 载入图文件
image = imread(argv[1], CV_LOAD_IMAGE_COLOR);
// 检查读取文件是否成功
if (!image.data)
{
cout << "无法打开或找不到图文件" << std::endl;
return -1;
}
// 建立显示图文件窗口
namedWindow("原图", CV_WINDOW_NORMAL);
namedWindow("下雪图", CV_WINDOW_NORMAL);
imshow("原图", image);
// 雪点数
int i = 600;
for (int k = 0; k < i; k++) {
// rand() is the MFC random number generator
// try qrand() with Qt
int i = rand() % image.cols;
int j = rand() % image.rows;
if (image.channels() == 1) { // gray-level image
image.at<uchar>(j, i) = 255;
if (i < (int)image.cols)
image.at<uchar>(j + 1, i) = 255;
if (j < (int)image.rows)
image.at<uchar>(j, i + 1) = 255;
if (i < (int)image.cols && j < (int)image.rows)
image.at<uchar>(j + 1, i + 1) = 255;
}
else if (image.channels() == 3) { // color image
image.at<cv::Vec3b>(j, i)[0] = 255;
image.at<cv::Vec3b>(j, i)[1] = 255;
image.at<cv::Vec3b>(j, i)[2] = 255;
if (i < (int)image.cols - 1)
{
image.at<cv::Vec3b>(j, i + 1)[0] = 255;
image.at<cv::Vec3b>(j, i + 1)[1] = 255;
image.at<cv::Vec3b>(j, i + 1)[2] = 255;
}
if (j < (int)image.rows - 1)
{
image.at<cv::Vec3b>(j + 1, i)[0] = 255;
image.at<cv::Vec3b>(j + 1, i)[1] = 255;
image.at<cv::Vec3b>(j + 1, i)[2] = 255;
}
if (j < (int)image.rows - 1 && i < (int)image.cols - 1)
{
image.at<cv::Vec3b>(j + 1, i + 1)[0] = 255;
image.at<cv::Vec3b>(j + 1, i + 1)[1] = 255;
image.at<cv::Vec3b>(j + 1, i + 1)[2] = 255;
}
}
}
// 在窗口内显示图文件
imshow("下雪图", image);
// 窗口等待按键
waitKey(0);
return 0;}
程序执行的结果对比,如图2-14所示。
(a)原图
(b)飘雪图
图2-14
本程序是示范图像矩阵内每一个点的处理。
2.2 图文件转换
图文件转换的代码如下:
#include <opencv\cv.h>#include <opencv\highgui.h>using namespace cv;int main(int argc, char* argv){
// 图文件
char* imageName = "C:\\images\\lena.jpg";
// 读取图文件
Mat image = imread(imageName, 1);
Mat gray_image;
// 图文件从BGR转成灰度
cvtColor(image, gray_image, CV_BGR2GRAY);
// 存储转换后的图文件
imwrite("C:\\images\\process\\Gray_lena.jpg", gray_image);
// 显示图文件窗口大小的控制
namedWindow(imageName, CV_WINDOW_AUTOSIZE);
namedWindow("Gray image", CV_WINDOW_AUTOSIZE);
// 显示原先图文件
imshow(imageName, image);
// 显示灰度图文件
imshow("C:\\images\\process\\Gray image", gray_image);
waitKey(0);
return 0;}
程序说明
1.cvtColor (InputArray src, OutputArray dst, int code, int dstCn=0):图像颜色空间 (color space) 转换
(1)src:输入图像。
(2)dst:输出图像。
(3)code:颜色空间转换种类。
RGB与CIE转换:CV_BGR2XYZ、CV_RGB2XYZ、CV_XYZ2BGR、CV_XYZ2RGB。
RGB与YCrCB JPEG转换:CV_BGR2YCrCb、CV_RGB2YCrCb、CV_YCrCb2BGR、CV_YCrCb2RGB。
RGB与HSV转换:CV_BGR2HSV、CV_RGB2HSV、CV_HSV2BGR、CV_HSV2RGB。
RGB与HLS转换:CV_BGR2HLS、CV_RGB2HLS、CV_HLS2BGR、CV_HLS2RGB。
RGB与CIE Lab*转换:CV_BGR2Lab、CV_RGB2Lab、CV_Lab2BGR、CV_Lab2RGB。
RGB与CIE Luv转换:CV_BGR2Luv、CV_RGB2Luv、CV_Luv2BGR、CV_Luv2RGB。
Bayer转换成RGB:CV_BayerBG2BGR、CV_BayerGB2BGR、CV_BayerRG2BGR、CV_BayerGR2BGR、CV_BayerBG2RGB、CV_BayerGB2RGB、CV_BayerRG2RGB、CV_BayerGR2RGB。
(4)dstCn:输出图像通道数,如果值为0,输出图像通道数由输入图像src与颜色空间code自动取得。
2.imwrite(const string& filename, InputArray img):存储图像
(1)filename:要存储的文件名。
(2)img:要存储的图像。
程序执行结果,图2-15(a)为原图彩色,图2-15(b)为转换后的灰度图。
(a)原图彩色
(b)灰度图
图2-15
2.3 图文件混合
图文件混合的代码如下:
#include "opencv2/highgui/highgui.hpp"#include <iostream>using namespace cv;int main(void){
double alpha, beta, input;
Mat src1, src2, dst;
/// 让用户输入 alpha 值
std::cout << " 简易线性混合(Linear Blender) " << std::endl;
std::cout << "-----------------------" << std::endl;
std::cout << "* 输入 0 到 1 的 alpha 值: ";
std::cin >> input;
// 确认 alpha 值数入的正确在于 0 与 1 之间
if (alpha >= 0 && alpha <= 1)
{
alpha = input;
}
/// 读取两个大小与类型相同的图文件
src1 = imread("C:\\images\\LinuxLogo.jpg");
src2 = imread("C:\\images\\WindowsLogo.jpg");
if (!src1.data)
{ std::cout << "读取 src1 错误" << std::endl; return -1; }
if (!src2.data)
{ std::cout << "读取 src2 错误" << std::endl; return -1; }
namedWindow("Linear Blend", 1);
beta = (1.0 - alpha);
addWeighted(src1, alpha, src2, beta, 0.0, dst);
imshow("Linear Blend", dst);
waitKey(0);
return 0;}
程序说明
addWeighted(InputArray src1, double alpha, InputArray src2, double beta, double gamma, OutputArray dst, int dtype=-1):以权重将两图合并
(1)src1:要相加的第一个图文件。
(2)alpha:第一个图文件的权重。
(3)src2:要相加的第二个图文件,因为只是单纯的与第一个图文件相加,所以大小与通道数要与第一个图文件相同。
(4)beta:第二个图文件的权重。
(5)gamma:两图相加后要再增加的值。
(6)dst:两图相加结果的图文件。
(7)dtype:相加结果图的景深(depth);此参数可有可无。
本程序是使用addWeighted函数实现下列公式的图像处理:
g(x)=(1−α)f0(x)+α f1(x);
式中,f0(x)是第一个图;f1(x)是第二个图,因为addWeighted在OpenCV内部就是dst=α·src1+β·src2+γ,只是公式中的γ 为0。
输入分别为0、0.5和1,对应的执行结果如图2-16所示。
图2-16
读者如果使用过Photoshop的图层,应该对此功能不陌生。
注意
虽然alpha与beta是两张图的权重,但其实可以是各小于1的值,而不用相加等于1。
商标特效
商标特效的代码如下:
#include <opencv2/core/core.hpp>#include <opencv2/highgui/highgui.hpp>#include "opencv2/imgproc/imgproc.hpp"#include <vector>using namespace cv;int main(){
// 载入图文件
Mat image1 = imread("C:\\images\\lena.jpg");
Mat logo =
imread("C:\\opencv\\build\\doc\\opencv-logo-white.png");
// 编译器要求使用前要给初始值
Mat image = image1, opencvlogo;
// 缩小原图成 Size(col, row)
resize(logo, opencvlogo, Size(80, 64));
namedWindow("Image 1", CV_WINDOW_AUTOSIZE);
// 定义图有兴趣的区域(Region Of Interest, ROI)
Mat imageROI;
// 指定商标在原图的位置,Rect(x, y, width(col), height(row))
imageROI = image(Rect(420, 420, 80, 64));
imshow("Image 1", opencvlogo);
// 加入商标
addWeighted(imageROI, 1.0, opencvlogo, 0.3, 0., imageROI);
// 显示结果
namedWindow("with logo");
imshow("with logo", image);
waitKey();
return 0;}
程序说明
1.resize(InputArray src, OutputArray dst, Size dsize, double fx=0, double fy=0, int interpolation=INTER_LINEAR ):改变图像大小
(1)src:输入图像。
(2)dst:输出图像。
(3)dsize:输出图像的图像大小,如果等于0就用下列公式计算。
dsize = Size(round(fxsrc.cols), round(fysrc.rows),但是fx与fy都不可等于0。
(4)fx:水平轴的缩放因子,如果等于0就用下列公式计算:
(double)dsize.width/src.cols。
(5)fy:垂直轴的缩放因子,如果等于0就用下列公式计算:
(double)dsize.height/src.rows。
(6)interpolation:插值方式。
INTER_NEAREST:最靠近周围插值法(nearest-neighbor)。
INTER_LINEAR:双线性插值法(bilinear)。
INTER_AREA:用像素关系区再取样插值法(resampling)。
INTER_CUBIC:在4×4像素附近用双立方插值法(bicubic)。
INTER_LANCZOS4:在8×8像素附近用Lanczos插值法。
2.size Mat::size() const:返回矩阵大小
size(cols, rows)也返回矩阵大小,同时指定矩阵的行列数。
执行结果如图2-17所示。
图2-17
addWeighted(src1, 0.7, src2, 0.9, 0.0, dst)又可以改写成如下的代码,根据读者喜好自行决定。
src1 = src1 * 0.7;
src2 = src2 * 0.9;
add(src1, src2, dst);
也可简单写成dst = src1 0.7 + src2 0.9,只是Add函数还有其他的参数可以使用。
雨天特效
雨天特效的代码如下:
#include <opencv2/core/core.hpp>#include <opencv2/highgui/highgui.hpp>#include "opencv2/imgproc/imgproc.hpp"#include <vector>using namespace cv;int main(){
Mat image1, image2, image3;
image1 = imread("C:\\images\\lake.jpg");
if (!image1.data)
return 0;
image2= imread("C:\\images\\fur.jpg");
if (!image2.data)
return 0;
// 以image2图像大小调整image1图像大小
resize(image2, image3, image1.size());
//显示原图
namedWindow("Image 1");
imshow("Image 1",image1);
namedWindow("Image 3");
imshow("Image 3",image3);
// 雨天特效图
Mat result;
image3 = image3 * 0.3;
image1 = image1 * 0.9;
add(image1, image3, result);
namedWindow("result");
imshow("result",result);
waitKey();
return 0;}
程序说明
add(InputArray src1, InputArray src2, OutputArray dst, InputArray mask=noArray(), int dtype=-1):计算两图像或颜色值相加
(1)src1:第一个图像或颜色值。
(2)src2:第二个图像或颜色值,与第一个图像同大小。
(3)dst:输出图像。
(4)mask:掩码(mask),可有可无。
(5)dtype:输出图像的景深,可有可无。
湖的原图和特效图如图2-18和图2-19所示。
图2-18
图2-19
雨天特效结果如图2-20所示。
特效图其实是从熊身上采集来的图,如图2-21所示。
图2-20
图2-21
本节程序是介绍图文件像素(pixel)的存储与处理。在说明程序之前,我们先介绍计算机系统中色彩的概念。像素是由颜色空间(color space)或通道(channel)与数据类型(data type)来描述的。对于最简单的黑白图像,我们调整颜色空间与数据类型就可以产生各种灰度的灰色图。
OpenCV像素的数据类型是以下列方式表达的:CV_ABCD。
A:每个像素多少位。
B:是否有正负号。
C:类型前置码。
D:通道数目。
例如,CV_8UC3表示如下。
A:每个像素为8位。
B:没有正负号。
C:因为8位没有正负号,所以使用 Char 来表示像素。
D:每个像素有3个通道。
RGB三原色使用CV_8UC3表示如下。
(255, 0, 0):红色。
(0, 255, 0):绿色。
(0, 0, 255):蓝色。
(0, 0, 0):黑色。
(255, 255, 255):白色。
所谓彩色就有很多方式的应用,利用各种的组合让颜色更多样化。一般色彩系统分为以下4种方式。
RGB:它是最普遍也是与肉眼最接近的,由红(R)、绿(G)、蓝(B)三色组成,屏幕显示都是用此系统,但注意通道的顺序是RGB。
HSV 与 HLS:系统将颜色分为色调(hue)、饱和度(saturation)和明亮度(luminance),是最自然说明色彩的方式。
YCrCb:主要用于JPEG图像。
CIE Lab*:这是概念上单一的色彩空间,用来计算色距(distance of color)最为方便。
扫描图文件快慢的比较
一般图都是R、G、B三原色。如果颜色太多就会造成图文件非常大,所以为了降低图文件大小都会采用褪色(color reduction)。褪色的目的是加快图文件的扫描进行图像处理,还要加快处理的速度。读者可参考C:\OpenCV\sources\samples\cpp\tutorial_code\core\how_to_scan_images的范例来了解该用法快慢的差异。
OpenCV的遮罩处理(mask operation)
这是根据遮罩值重算图像中的每个像素。例如图像处理方式的公式如下:
I(i, j) = 5×I(i, j)−[ I(i−1, j) + I(i+1, j) + I(i, j−1) + I(i, j+1)]
而遮罩的方式则是:
OpenCV提供filter2D函数来进行遮罩处理。
读者可以参考C:\OpenCV\sources\samples\cpp\tutorial_code\core\mat_mask _operations的范例程序来了解如何进行遮罩处理。
2.4 改变对比与明亮度
改变对比与明亮度的代码如下:
#include <opencv/cv.h>#include <opencv/highgui.h>#include <iostream>using namespace cv;double alpha; // 对比控制int beta; // 明亮度控制int main(int argc, char** argv){
Mat image = imread("C:\\images\\lena.jpg");
Mat new_image = Mat::zeros(image.size(), image.type());
/// Initialize values (Basic Linear Transform)
std::cout << " 基本线性转换" << std::endl;
std::cout << "-------------------------" << std::endl;
std::cout << "* 输入 alpha 值 [1.0-3.0]: "; std::cin >> alpha;
std::cout << "* 输入 beta 值 [0-100]: "; std::cin >> beta;
/// 转换公式 new_image(i,j) = alpha*image(i,j) + beta
for (int y = 0; y < image.rows; y++)
{
for (int x = 0; x < image.cols; x++)
{
for (int c = 0; c < 3; c++)
{
// 针对像素的每个通道做转换
new_image.at<Vec3b>(y, x)[c] =
saturate_cast<uchar>(alpha*(image.at<Vec3b>(y, x)[c]) + beta);
}
}
}
namedWindow("Original Image", 1);
namedWindow("New Image", 1);
imshow("Original Image", image);
imshow("New Image", new_image);
waitKey();
return 0;}
程序说明
图像转换可以使用两种方式:
点(point)的处理。
区块(Neighborhood area-based)处理。
明亮与对比的调整就属于点的处理,而点的处理最普遍就是加乘处理。
g(x)=αf(x)+β
Mat new_image = Mat::zeros( image.size(), image.type() );为了不影响原图,先建立一个大小与原图相同的矩阵,内容先补0。
new_image.at<Vec3b>(y,x)[c] =saturate_cast<uchar>( alpha*( image.at<Vec3b>(y,x)[c] ) + beta );
图像处理使用3个for循环处理图像的每个点,第一个是行、第二个是列、第三个是通道。因为原图是彩色的,所以处理3个通道(R,G,B)需要3个循环。由于是对图像的每一个点进行处理,所以时间就会久一点。
在图像点的指定是使用image.at<Vec3b>(y,x)[c],y是列、x是行,c就是R、G、B(0、1、2),R = 0,以此类推。
这3层循环程序也可以简化成为如下单一指令。
image.convertTo(new_image,-1, alpha, beta);执行结果alpha = 2.2,beta = 50,如图2-22所示。
图2-22
本文摘自图书《OpenCV和Visual Studio图像识别应用开发》
点击阅读原文试读更多章节