新坑。学习下如何使用OpenCV。
使用教材,《OpenCV3编程入门》—毛星云 电子工业出版社。(感谢毛佬留下的宝贵知识财富!
使用的IDE。Visual Studio 2022+OpenCV 4.6.0。环境什么的安装细节很多blog都有,不多赘述。
旧坑还在继续加油,但是难度比较大,一是因为教授的飞机语速,二是由于确实内容比较难懂。只能说,菜鸟会尽力写完的。
别质疑,先相信!
原理:卷积核和图像按位相乘,并滑动。将计算结果中的最小值赋予中心像素作为结果。
实现:卷积,若全部按位相乘的结果都不为0,则结果为1,否则为0
效果:(对二值图像)图像中白的色块减少,黑色增加。
腐蚀可以清除掉图像的一些毛刺和细节,腐蚀一般也可以用来消除噪点
#include
#include
#include
using namespace cv;
int main()
{
Mat image = imread("test.jpg"); //载入图像到test
imshow("【原始图】", image);
Mat element = getStructuringElement(MORPH_RECT, Size(15, 15)); //设定卷积核大小为15x15的正方形,元素均为1
std::cout << element;
Mat result;
erode(image, result, element);
imshow("【结果】", result);
waitKey(0);
return 0;
}
PS:ptr是opencv中用于提取矩阵的指针,该指针可以按行或按列访问矩阵的像素数据。
例子:char *data = x.ptr
下面是对erode函数的参数的说明(不感兴趣可以不看):
dst = cv2.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,输入图像,即源图像,填Mat类的对象即可。图像通道的数量可以是任意的,但图像深度应为CV_8U,CV_16U,CV_16S,CV_32F或 CV_64F其中之一。
第二个参数,OutputArray类型的dst,处理结果。需要和源图片有一样的尺寸和类型。
第三个参数,InputArray类型的kernel,膨胀操作的核。若为NULL时,表示的是使用参考点位于中心3x3的核。
实现:用一个结构元素(一般是3×3的大小)扫描图像中的每一个像素,用结构元素中的每一个像素与其覆盖的像素做“与”操作,如果都为0,则该像素为0,否则为1。
效果:(对二值图像)图像中黑的色块减少,白色增加。
#include
#include
#include
#include
#include //引用这个库,就可以避免CV_BGR2GRAY在OpenCV4中报错
using namespace cv;
int main()
{
Mat image = imread("test.jpg"); //载入图像到test
imshow("【原始图】", image);
Mat edge, grayImage;
cvtColor(image, grayImage, CV_BGR2GRAY);
//原图转化为灰度图像
blur(grayImage, edge, Size(3, 3));
//3x3平均滤波器去噪
Canny(edge, edge, 3, 6, 3);
//Canny算子
imshow("【结果】", edge);
waitKey(0);
return 0;
}
对于OpenCV中的颜色转换函数——void cvtColor(srcimage,dstimage, int code, int dstCn=0)
第一个参数是输入图像
第二个参数是输出图像
第三个参数是颜色空间转换的标识符
第四个参数为目标图像的通道数,若该参数是0,表示目标图像取源图像的的通道数。
值得留意的是:opencv默认的图片通道存储顺序是BGR,即蓝绿红,不是RGB
VideoCapture是OpenCV 2.X中的一个类,对应于C语言中的CvCapture结构体。
#include
using namespace cv;
int main()
{
VideoCapture capture("1.avi");
/*创建变量后读入视频,也可以分为以下两步操作:
VideoCapture capture;
capture.open(“1.avi”);
*/
while(1)
{
Mat frame;
capture >> frame; //读取当前帧
if (frame.empty()) //播放完成则退出循环
{
break;
}
imshow("读取视频",frame);
waitKey(30);
}
return 0;
}
和读取视频的区别就是VideoCapture capture(x)的参数x改成0——表示调用摄像头。
在OpenCV文件夹里的“.......\opencv\sources\samples\cpp\tutorial_code”有官方示例程序。其内容按照组件模块分类,适合学习。
“.......\opencv\sources\samples\cpp\tutorial_code”中找到Camshiftdemo.cpp。复制黏贴新建一个文件就可以运行了。
PS:vs2017及以上的版本已经基本包含CMake的功能了,所以可以不用再下载CMake了。
而解决方案可以直接生成。(.sln文件)
在查看OpenCV的示例程序时,经常看到int argc和char *argv [ ] 。其中arg指的是argument(参数),用于统计运行程序时送给main函数的命令行参数的个数;argv加上*和 [ ] 成为*argv[ ] 。表示字符串数组,用于存放指向字符串参数的指针数组,每个元素指向一个参数。
printf()函数一般调用形式为 int printf(const char *format,...)。括号的前面的参数是希望输出的格式,后面是输出的表列。
printf有“格式字符串”的定义——用于指定输出格式。格式字符串 都是以%开头的字符串,在%之后跟有各种格式字符。
包括:
格式字符串 | 作用 |
%d | 将整数转成十进制 |
%f | 将整数转换成浮点数 |
%u | 十进制无符号整数 |
%o |
将整数转换成八进制 |
%c | 将整数转换成对应的ASCII字符 |
%s |
将整数转换成字符串 |
%x |
整数转换为小写十六进制 |
%X | 整数转换为大写十六进制 |
%p | 输出地址符 |
%% | 输出百分比符号,不进行转换 |
除了格式字符串,还有一些特殊规定的字符,如下:
规定字符 | 作用 |
\n | 换行 |
\f | 清屏并且换页 |
\r | 回车 |
\t | Tab符号(4个空格) |
\xhh | 用16进制表示的ASCII码,其中每个h可以用0~f中的一个代替 |
如有多种对输出格式的设置,则在%后直接添加,无需添加%。
HighGUI模块是高层GUI图形用户界面模块,包含媒体输入输出、视频捕捉、图像和视频的编码解码、图形交互界面的接口等内容。第三章中我们将学习到,一些常用的交互操作,包括图像载入、显示和输出、为程序添加滑动条、鼠标操作等。
OpenCV中C++类和函数都是定义在命名空间cv之内的,有两种方法可以调用:其一,在代码#include库完毕后,添加using namespace cv;的代码;否则,在每一次调用OpenCV的类和函数时,都要在前面添加上 cv:: ,这种情况非常繁琐,很不推荐。(但是第一种方法使用之后,使用cout等std的函数和类的时候需要添加 std:: )
为了方便使用,OpenCV使用Mat类作为数据结构进行图像存取。默认大小为0。当然,在初始化时也可以指定其初始尺寸,比如定义一个Mat类对象,cv::Mat pic(320,640,cv::Scalar(100));
一般来说,我们可以用Mat srcImage = imread(“1.jpg”)来将文件名为“1.jpg”的图像文件加入到名为srcImage的Mat变量中。
其原型为 Mat imread(const string&filename,int flags = 1)。
第一个参数填入待载入文件的路径。(imrad支持.bmp、dib、jpeg、jpg、jpe、jp2、png、pbm、pgm、ppm、sr、ras、tiff、tif格式的文件)
第二个参数指定加载图像的颜色类型,默认为1,对应三通道彩色图像。为了方便记忆,这里就直接给出我们经过查找定义以及分析总结得到的结论。
flags>0,返回三通道彩色图像
flags=0,返回灰度图
flags<0,返回包含Alpha通道的加载图像
另外,请记住,彩色图像通道存储的方式是BGR而非RGB。
原型: void imshow(const string&winname,InputArray mat)
第一个参数:填写需要显示的窗口标识名。默认为CV_WINDOW_AUTOSIZE,即显示原始图像大小。否则图像会缩放以适应窗口。(缩放取决于图像深度,规则如下:
若图像是8位无符号,显示原图
若16位无符号或32位整型,则用像素值除以256。即将灰度值映射到【0~255】
若是32位浮点型,用像素值乘以255。亦即映射到【0-255】
第二个参数:填需要显示的图像
详细定义较长。大多情况,将InputArray/OutArray类型当作Mat类型即可。
namedWindow函数用于创建一个窗口。如果只是简单进行图片显示,直接使用imread和imshow即可。但是如果需要在显示窗口之前就用到窗口名时(比如滑动条的使用),就需要用到namedWindow。
原型如下: void namedWindow(const string&winname,int flags-WINDOW_AUTOSIZE);
第一个参数,填写被用作窗口标识符的窗口名称。
第二个参数,是窗口标识。可以是如下几种值
WINDOW_NORMAL,用户可以改变窗口大小
WINDOW_AUTOSIZE(默认值),窗口大小自动适应图像大小,用户无法手动改变
WINDOW_OPENGL,窗口创建时会支持OpenGL
namedWindow是通过指定的名字,创建一个可以作为图像和进度条的容器窗口。(如已经存在相同名称的窗口,则不执行任何操作)。同样的,我们可以使用destroyWindow()或者destroyAllWindow()来关闭窗口,并取消之前分配的与窗口相关的内存空间。
原型:bool imwrite(const string& filename,InputArray img,const vector
第一个参数,const string&类型的filename,填需要写入的文件名(带后缀),如“1.jpg”
第二个参数,InputArray类型的img,一般填一个Mat类型的图像数据(也就是待输出的图像)
第三个参数,一般无需填写,默认为vector
对JPEG图片,参数表示从0-100的图片质量,默认95
对PNG,表示压缩级别,从0-9。越高,尺寸越小,压缩时间越长
对PPM、PGM、PBM,表示一个二进制格式标志,取0或1,默认1
滑动条(Trackbar也叫做轨迹条)是OpenCV动态调节参数特别好用的一种工具,依附于窗口存在。(由于OpenCV没有按钮的功能,很多时候,可以用仅含0-1的滑动条实现按钮的按下和弹起的效果)
createTrackerbar()经常和一个回调函数配合使用。
createTrackerbar()函数原型:int createTrackerbar(const string& trackbarname, const string&winname, int* value, int count, TrackbarCallback onChange = 0, void* userdat = 0);
第一个参数,trackbarname是轨迹条的名字,用来代表创建的轨迹条。
第二个参数,winname是窗口的名字,表示轨迹条依附的窗口名字,即对应namedWindow()创建窗口时填的某一个窗口名
第三个参数,int* 类型的value,指针,表示滑块的位置。滑块初始位置就是该变量当前的值。
第四个参数,int的count,表示滑块可以达到的最大位置的值,最小位置的值始终为0
第五个参数,TrackbarCallback类型的onChange(默认值为0)。是一个指向回调函数的指针,每次滑块位置改变,这个函数都会回调。
第六个参数,void*类型的userdata(默认值为0),是用户传给回调函数的数据,用于处理轨迹条事件。若第三个参数value实际参数是全局变量的话,可以无视此参数
createTrackerbar()为我们创建了一个显示在winname(第二个参数)窗口上的有名称和范围的轨迹条(Trackbar,或者说是滑块范围控制工具),指定一个和轨迹条位置同步的变量,而且指定onChange(第五个参数),在轨迹条位置改变时调用这个回调函数。
回调函数的定义:简单来说,就是一个通过函数指针调用的函数。如果我们把函数的指针(地址)作为参数传回给另一个函数,当这个指针被用来调用其所指向的函数时,就是回调函数。回调函数不由该函数的实现对象直接调用,而是在特定事件或条件发生时由另外一方调用,作为对该事件的响应。下面进行较为详细的解释
#include
// 回调函数原型
typedef void (*CallbackFunction)(int);
// 具体函数,接受回调函数作为参数
void performOperation(int value, CallbackFunction callback) {
// 执行某些操作
// ...
// 在适当的时候调用回调函数
callback(value);
}
// 回调函数实现
void callbackFunction(int value) {
std::cout << "Callback executed with value: " << value << std::endl;
}
int main() {
int data = 42;
performOperation(data, callbackFunction); // 传递回调函数作为参数
return 0;
}
在上面的示例中,performOperation
函数接受一个整数参数和一个回调函数(通过函数指针来表示)。在 performOperation
函数内部,可以执行一些操作,并在适当的时候调用传递的回调函数。
在 main
函数中,我们定义了一个整数变量 data
,然后调用 performOperation
函数,并传递 data
和 callbackFunction
作为参数。当 performOperation
函数执行到适当的位置时,会调用传递的回调函数 callbackFunction
,并将 data
作为参数传递给回调函数。
通过使用回调函数,我们可以灵活地在程序执行期间插入自定义的逻辑,并根据需要处理特定的事件和操作。这种设计模式使得程序能够更灵敏地响应不同的情景和需求,并提高了代码的可扩展性和可维护性。
下面是一个关于createTrackbar的小例子。
createTrackbar("对比度:","【效果图窗口】",&g_nContrastValue,300,on_Change);
//g_nContrastValue为全局的整型变量,on_Change为回调函数的函数名(PS:在C/C++中,函数名就是指向函数地址的指针)
完整代码如下:
#include
#include"opencv2/highgui/highgui.hpp"
using namespace cv;
using namespace std;
#define WINDOW_NAME "【线性混合示例】" //为窗口标题定义的宏
//-----------------------【全局变量声明部分】------------------------
// 描述:全局变量声明
//-------------------------------------------------------------------
const int g_nMaxAlphaValue = 100;//Alpha的最大值
int g_nAlphaValueSlider;//滑动条对应的变量
double g_dAlphaValue;
double g_dBetaValue;
//声明存储图像的变量
Mat g_srcImage1, g_srcImage2, g_dstImage;
//-----------------------【on_Trackbar函数】-----------------------
// 描述:响应滑动条的回调函数
// ----------------------------------------------------------------
void on_Trackbar(int, void*)
{
//求出当前alpha值相对于最大值的比例
g_dAlphaValue = (double)g_nAlphaValueSlider / g_nMaxAlphaValue;
//则beta值为1减去alpha值
g_dBetaValue = (1.0 - g_dAlphaValue);
//根据alpha和beta值进行线性混合
addWeighted(g_srcImage1, g_dAlphaValue, g_srcImage2, g_dBetaValue, 0.0, g_dstImage);
//显示效果图
imshow(WINDOW_NAME, g_dstImage);
}
//---------------------【main()函数】----------------------------
// 描述:控制台应用程序的入口函数,我们的程序从这里开始执行
//-----------------------------------------------------------------
int main(int argc, char** argv)
{
//加载图像(两张图像的尺寸应相同)
g_srcImage1 = imread("1.jpg");
g_srcImage2 = imread("2.jpg");
if (!g_srcImage1.data)
{
printf("读取第一幅图片错误,请确定目录下是否由imread函数指定图片存在! \n");
return -1;
}
if (!g_srcImage2.data)
{
printf("读取第二幅图片错误,请确定目录下是否由imread函数指定图片存在! \n");
return -1;
}
//设置滑动条初值为70
g_nAlphaValueSlider = 70;
//创建窗口
namedWindow(WINDOW_NAME, 1);
//在创建的窗口中创建一个滑动条控件
char TrackbarName[50];
sprintf(TrackbarName, "透明值 %d", g_nMaxAlphaValue);
createTrackbar(TrackbarName, WINDOW_NAME, &g_nAlphaValueSlider, g_nMaxAlphaValue, on_Trackbar);
//结果在回调函数中显示
on_Trackbar(g_nAlphaValueSlider, 0);
//按任意键退出
waitKey(0);
return 0;
}
附:这里出了点问题,我把第二个imread的文件名的 “.” 打成 “,”了,检查了半天,甚至把if中换成了.empty()来确保不是判断的问题。最后发现是文件名打错了。。。大家要多多检查。。。
那么上面这个程序执行的结果就是,生成一个名为【线性混合示例】的窗口,有一个滑动条可以调节两张图片混合时各自的透明度,具体效果如下图。
getTrackbarPos()是和createTrackbar配合使用的函数,用于获取当前轨迹条的位置。函数原型为:int getTrackbarPos( const string& trackbarname, const string& winname );
第一个参数,const string&类型的trackbarname,是轨迹条的名字
第二个参数,const string&类型的winname,表示轨迹条所属的窗口名称
OpenCV中鼠标操作和滑动条的消息映射方式类似,也是通过一个中介函数配合一个回调函数来实现的。创建和指定滑动条回调函数的函数是createTrackbar,同样的,对于鼠标,函数是SetMouseCallback。
函数原型是:void SetMouseCallback( const string& winname, MouseCallback onMouse, void* userdata=0 );
第一个参数,窗口的名字
第二个参数,指定窗口内每次鼠标事件发生的时候,被调用的函数指针。
第三个参数,void*类型的userdata(默认为0),用户定义的传递到回调函数的参数
示例程序如下:
#include
using namespace cv;
#define WINDOW_NAME "程序窗口" //为窗口标题定义的宏
//全局函数声明
void on_MouseHandle(int event, int x, int y, int flags, void* param);
void DrawRectangle(Mat& img, Rect box);
void ShowHelpText();
//全局变量声明
Rect g_rectangle;
bool g_bDrawingBox = false; //是否进行绘制
RNG g_rng(12345);
int main()
{
//【1】准备参数
g_rectangle = Rect(-1, -1, 0, 0);
Mat srcImage(600, 800, CV_8UC3), tempImage;
srcImage.copyTo(tempImage);
g_rectangle = Rect(-1, -1, 0, 0);
srcImage = Scalar::all(0);
//【2】设置鼠标操作回调函数
namedWindow(WINDOW_NAME);
setMouseCallback(WINDOW_NAME, on_MouseHandle, (void*)&srcImage);
//【3】程序主循环,当进行绘制的标识符为真时,进行绘制
while (1)
{
srcImage.copyTo(tempImage);//复制源图到tempImage
if (g_bDrawingBox)
DrawRectangle(tempImage, g_rectangle);//当绘制的标识符为真,则进行绘制
imshow(WINDOW_NAME, tempImage);
if (waitKey(10) == 27)
break;
}
return 0;
}
//------------------------on_MmouseHandle()函数--------------------------------------
//-------------------描述:鼠标回调函数,根据不同鼠标事件进行不同操作----------------
//-----------------------------------------------------------------------------------
void on_MouseHandle(int event, int x, int y, int flags, void* param)
{
Mat& image = *(Mat*)param;
switch (event)
{
//鼠标移动消息
case EVENT_MOUSEMOVE:
{
if (g_bDrawingBox)//如果绘制标识符是真,则记录下长和宽到RECT型变量中
{
g_rectangle.width = x - g_rectangle.x;
g_rectangle.height = y - g_rectangle.y;
}
}
break;
case EVENT_LBUTTONDOWN:
{
g_bDrawingBox = true;
g_rectangle = Rect(x, y, 0, 0);//记录起始点
}
break;
case EVENT_LBUTTONUP:
{
g_bDrawingBox = false;//将标识符置为false
//对高和宽小于0的处理
if (g_rectangle.width < 0)
{
g_rectangle.x += g_rectangle.width;
g_rectangle.width *= -1;
}
if (g_rectangle.height < 0)
{
g_rectangle.y += g_rectangle.y;
g_rectangle.height *= -1;
}
//调用函数进行绘制
DrawRectangle(image, g_rectangle);
}
break;
}
}
//------------------【DrawRectangle()函数】-----------------
// 描述:自定义矩形绘制函数
//----------------------------------------------------------
void DrawRectangle(Mat& img, Rect box)
{
rectangle(img, box.tl(), box.br(), Scalar(g_rng.uniform(0, 255), g_rng.uniform(0, 255), g_rng.uniform(0, 255)));
//随机颜色
}
最终实现效果如下图
简单来说,我们可以通过按下鼠标左键和松开来在窗口中绘制彩色的矩形。而程序中的on_MouseHandle就是回调函数。其中一个switch语句定义了对于不同的鼠标事件进行对应的操作。
对于OpenCV 1.X时代的基于C语言接口而建的图像存储格式IplImage*,如果在退出前没有及时release掉,会造成内存泄露。但C++的出现带来了类的概念。
对于Mat类,需要知道以下两点:
1、不必手动为其开辟空间(实际上,大多数OpenCV函数仍然会手动为输出数据开辟空间,每次传递一个以存在的Mat对象时,开辟好的的矩阵空间会被重新使用)
2、不必在不需要时立即将空间释放
总而言之,Mat这个类由两个数据部分组成——矩阵头(包含矩阵尺寸、存储方法、存储地址等信息)和一个 指向存储所有像素值的矩阵的 指针。矩阵头尺寸是常数值,但矩阵尺寸会根据图像的不同而不同,通常比矩阵头的尺寸大数几个数量级。因此,在程序中传递图像并创建副本时,大的开销是由矩阵造成的,而非信息头。OpenCV是一个DIP库,囊括了大量函数,而使用函数时难免需要在其中传递图像,所以,应尽量减少大图像的复制。
为了解决上述问题,OpenCV有引用计数机制。其思路是让每个Mat对象有自己的信息头,但是共享同一个矩阵。是通过让矩阵指针指向同一地址而实现,而拷贝构造函数则只复制信息头和矩阵指针。请看以下代码:
Mat A,C; //仅创建信息头部分
A = imread("1.jpg",CV_LOAD_IMAGE_COLOR); //为矩阵开辟内存
Mat B(A); //使用拷贝构造函数
C = A; //赋值运算符
以上代码中所有Mat对象都指向一个数据矩阵。虽然信息头不同,但通过任何一个对象所做的改变也会影响其他对象。这里有一个功能:可以创建只引用部分数据的信息头。比如,创建一个感兴趣区域(ROI),只需要创建包含边界信息的信息头
Mat D(A,Rect(10, 10, 100, 100) );//矩形界定
Mat E = A(Range:all(), Range(1,3) );//行和列界定
对于一个属于多个Mat对象的矩阵,最后一个使用它的对象,负责清理(通过引用计数机制实现)。复制Mat对象的信息头时,会增加矩阵的引用次数,当一个头被释放之后,计数减一;计数为0,矩阵就会被清理。但有些时候,你会想复制矩阵本身(不只是信息头和矩阵指针),可以使用clone()或者copyTo()。
Mat F = A.clone();
Mat G;
A.copyTo(G);
这样一来,改变F或者G就不影响A信息头指向的矩阵。
本小节可总结为4个要点:
1、OpenCV函数中输出图像的内存分配是自动完成的。
2、使用OpenCV的C++接口无需考虑内存释放问题
3、赋值运算符(“=”)和拷贝构造函数(构造函数)只是复制信息头
4、使用clone()、copyTo()复制一幅图像的矩阵
存储像素值需要指定颜色空间和数据类型。其中,颜色空间是指针对一个给定的颜色,如何组合颜色元素以对其编码。最简单的颜色空间是灰度级空间,只处理黑色和白色。
对于彩色,则有更多种类的颜色空间。但都是将颜色分成三或四个基本元素。RGB颜色空间就是最常用的颜色空间。基色是R(红)、G(绿)、B(蓝),有时为了表示透明颜色有第四个元素alpha(A)。每个组成元素都有其定义域(取决于其数据类型)。最小的是char,占一个字节或者8位,可以是有符号型(0~255)或者无符号(uchar,-128~+127)。
在第一章里,我们学习过imwrite()将一个矩阵(图像)写入到图像文件中。但是如果是debug,观看实际值会更加方便,我们可以通过Mat的运算符 “ << ” 实现,但是,“ << ”只对二维矩阵有效。
Mat不但是一个非常有用的图像容器类,同时也是一个通用的矩阵类,我们也可以用它创建和操作多维矩阵。创建方法有多种,如下:
【方法一】使用Mat()构造函数
最常用的方法是直接用Mat构造函数()。
Mat M(2,2, CV_8UC3, Scalar(0,0,255));
cout<<"M = "<
程序运行结果如下:
对于输出结果,是对于Mat的默认输出形式。先存储第一个元素的三个通道数据,再存储第二个元素三个通道数据。对于同一行的不同元素不予“ ; ”分隔,而对不同行直接,是以“ ; ”进行分割的。
对于二维多通道图像,首先定义其尺寸,亦即行数和列数,最后,需要指定存储数据类型以及每个矩阵点的通道数。为此,根据下面的规则有多种定义:
CV_[The number of bits per item][Signed or Unsigned][Type Prefix]C[The channel number]
即CV_[位数][带符号与否][类型前缀]C[通道数]
比如CV_8UC3表示使用8位的unsigned char型,每个像素由三个元素组成三通道。而预先定义的通道数可以多达四个。另外,Scalar是个short型的向量,能够使用指定的定制化值来初始化矩阵,还能用于表示颜色(后文会详细讲解)。若是需要更多通道数,可以用大写宏将通道数放在小括号中,如方法二中代码所示
【方法二】在C\C++中通过构造函数进行初始化
这种方法是在C\C++中通过构造函数初始化
int sz[3] = {2,2,2};
Mat L(3,sz,CV_8UC3, Scalar::all(0));
这段代码演示了如何创建一个超过两维的矩阵:指定维数,然后传递一个指向一个数组的指针,这个数组包含每个维度的尺寸;后续的两个参数与方法一中的相同
PS:这段代码输出时有错误,如你所见,“春春的菜鸟”无法解决。留给有缘人,解决了可以评论、私信告诉我,感谢!
【方法三】为已存在的Ipllmage指针创建信息头
方法三是为已存在的iplimage(为了方便阅读,我将其改为小写)指针创建信息头,示范代码如下:
IplImage* img = cvLoadImage("1.jpg",1);
Mat mtx(img); //转换IplImage*->Mat
PS: IplImage是OpenCV中CxCore部分基础的数据结构,用来表示图像,其中Ipl是Intel Image Processing Library的简写。OpenCV2.1版本之前使用IplImage*数据结构来表示图像,2.1之后的版本使用图像容器Mat来存储。IplImage结构体如下所示。
【方法四】利用create()函数
利用Mat类中的create()成员函数进行Mat类的初始化操作,示范代码如下:
M.create(4,4,CV_8UC2);
cout<<" M = "<
此方法不能为矩阵设置初始值,只是在改变尺寸时重新为矩阵数据开辟内存而已。
【方法五】采用Matlab式的初始化方式
方法五采用matlab形式的初始化方法。zeros()、ones()、eye()。
zeros(行,列,格式)
ones()和eye()的使用格式同上,而后者是生成单位矩阵。
【方法六】对小矩阵使用逗号分隔式初始化函数
Mat C = (Mat_(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
运行结果是生成了一个3行3列的矩阵,元素为 “ << ” 后面的序列。
【方法七】为已存在的对象创建新信息头
使用clone()、或者copyTo()为已经存在的Mat对象创建一个新的信息头,但是实际上二者还是共享一个矩阵。
Mat RowClone = C.row(1).clone();
cout<< "RowClone = " <
OpenCV提供了风格各异的格式化输出方法。这一节将一一演示和列举。
首先是下面代码中将要使用的 r 矩阵的定义。需要注意,我们可以通过用randu()产生的随机值来填充矩阵——当然,需要上下限来确保随机值在一定范围内。
Mat r = Mat(10,3,CV_8UC3);
randu(r, Scalar::all(0), Scalar::all(255)); //上下限是(255,255,255)和(0,0,0)
输出风格有以下几种:
【一】OpenCV默认风格
【二】Python风格
【三】逗号分隔分隔(Comma separated values,CSV)
【四】Numpy风格
【五】C语言风格
#include
using namespace cv;
using namespace std;
int main()
{
Mat r = Mat(10,3,CV_8UC3);
randu(r, Scalar::all(0), Scalar::all(255)); //上下限是(255,255,255)和(0,0,0)
cout << "r (OpenCV默认风格) = \n" << r << ";" << endl << endl;
cout << "r (Python风格) = \n" << format(r, Formatter::FMT_PYTHON) << ";" << endl << endl;
cout << "r (逗号分隔风格) = \n" << format(r, Formatter::FMT_CSV) << ";" << endl << endl;
cout << "r (Numpy风格) = \n" << format(r, Formatter::FMT_NUMPY) << ";" << endl << endl;
cout << "r (C语言风格) = \n" << format(r, Formatter::FMT_C) << ";" << endl << endl;
return 0;
}
除了Mat类型,OpenCV同样支持使用 “<<” 打印其他常用OpenCV数据结构。
1.定义和输出二维点
2.定义和输出三维点
3.定义和输出基于Mat的std::vector
4.定义和输出std::vector
#include
using namespace cv;
using namespace std;
int main()
{
Point2f p(6, 2);
cout << "【二维点】p=" << p << ";\n" << endl;
Point3f p3f(8, 2, 0);
cout << "【二维点】p=" << p3f << ";\n" << endl;
vector v;
v.push_back(3);
v.push_back(5);
v.push_back(7);
cout << "【基于Mat的vector】shortvec = \n" << Mat(v) << ";\n" << endl;
vector points(20);
for (size_t i = 0;i < points.size();++i)
points[i] = Point2f((float)(i * 5), (float)(i % 7));
cout << "【二维点向量】points = \n" << points << ";";
return 0;
}
Point类数据结构表示了二维坐标系下的点,即由其图像坐标x和y指定的2D点。用法如下:
Point point;
point.x = 10;
point.y = 8;
//或者是
Point point = Point(10,8);
//另外,在OpenCV中有如下定义:
typedef Point_ Point2i;
typedef Point2i Point;
typedef Point_ Point2f;
所以Point_
Scalar()表示具有4个元素的数组,在OpenCV中被大量用于传递像素值,例如最常用的RGB。对于Scalar函数来说,如果不需要第四个参数(除了R、G、B),则无需写出来;如果只写3个参数,OpenCV认为我们只想表示3个参数。
下面是个例子。
Scalar(a,b,c); //红色分量为c,绿色分量为b,蓝色分量为a
PS:Scalar类的源头为Scalar_类,而Scalar_是Vec4x的一个变种,常用的Scalar就是Scalar_
通过在代码中对Siz类进行“转到定义”操作,我们可以在........\opencv\sources\modules\core\include\opencv2\core\core.hpp路径下,找到Size类的相关源代码(注意,由于OpenCV版本不同,路径可能稍有不同,但是core.hpp的文件名应当还是未改变的)
typedef Size_ Size2i;
typedef Size2i Size;
其中,Size_是个模板类,Size_
追根溯源到Size_模板类的定义,可以发现内部重载了一些构造函数,其中使用频率最高的是下面这个
Size_(_Tp _width,_Tp _height)
_Tp width,height;//宽度和高度
//例子:构造出的Size宽度高度都为5
Size(5,5)
Rect类的成员变量有x、y、width、height,分别为左上角点的坐标和矩形的宽和高。常用的成员函数有:Size()return Size;area()return矩形的面积;contains(Point)判断点是否在矩形内;inside(Rect)函数判断矩形是否在该矩阵内;tl()return左上点角坐标;br()返回右下角点坐标。值得注意的是,如果想求两个矩形的交集和并集,可用如下格式:
Rect rect = rect1 & rect2;
Rect rect = rect1 | rect2;
//如果想让矩形进行平移操作和缩放操作,甚至可以这也写
Rect rectShift = rect + point;
Rect rectScale = rect + size;
cvtColor()是OpenCV里的颜色空间转换函数,可以实现RGB颜色向HSV、HSI等颜色空间的转换,也可以转换为灰度图像。
原型如下:
viod cvtColor(InputArray src, OutputArray dst, int code, int dstCn = 0)
第一个参数为输入图像,第二个为输出图像,第三个参数是颜色空间转换的标识符(具体可查对应表格,本书P98),第四个是目标图像的通道数,若该参数是0,表示目标图像通道数取自源图像。下面是调用示例:
//OpenCV2 Version
cvtColor(srcImage,dstImage,CV_GRAY2BGR)
//OpenCV3 Version
cvtColor(srcImage,dstImage,COLOR_GRAY2BGR)
不难发现,在OpenCV2中,标识符的前缀多为CV_,而在OpenCV3中被COLOR_的宏命名形式取代。另外,再次提醒一下,OpenCV默认存储为BGR。
本节最后,使用下面的示例代码,进行简单的cvtColor函数的使用。
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include
using namespace cv;
using namespace std;
void main()
{
//【1】载入图片
Mat src = imread("1111.jpg", 1), dst;
//【2】转换颜色空间
cvtColor(src, dst, COLOR_BGR2Lab);
//【3】显示效果图
imshow("效果图",dst);
//【4】保持窗口显示
waitKey();
}
本小节列举一些OpenCV的core模块中其他常用的知识点,如下。
1.Matx是一个轻量级的Mat,使用前规定好大小,比如2*3的float型的Matx,可以声明为Matx23f。
2.Vec是Matx的一个派生类,是一个一维的Matx,跟vector很相似,有如下定义。
template class Vec: public Matx<_Tp, n, 1>{...};
typedef Vec Vec2b;
3.Range类其实就是为了使OpenCV的使用更像Matlab而产生的。比如,Range::all()其实就是Matlab中的符号。而Range(a,b)其实就是Matlab中的a:b,注意此处的a、b都为整型。
4.OpenCV中防止内存溢出的函数有alignPtr、alignSize、allocate、deallocat、fastMalloc、fastFree等
5.
6.显示文字相关的函数:grtTextSize、cvInitFont、putText
7.作图相关的函数:circle、clipLine、ellipse、ellipse2Poly、line、rectangle、polylines、类LineIterator
8.填充相关的函数:fillConvexPoly、fillPoly
9.OpenCV中RNG()函数能够为初始化随机数状态的生成器
本节主要讲述使用point在图像中定义2D点、如何使用Scalar表示颜色的值。涉及到以下函数:
·绘制直线——line
·绘制椭圆——ellipse
·绘制矩形——rectangle
·绘制圆——circle
·绘制填充多边形——fillPoly
#define WINDOW_WIDTH 600 //定义窗口大小的宏
//--------------------【DrawEllipse()】-------------------
// 描述:自定义的绘制函数,实现了绘制不同角度、相同尺寸的椭圆
//-------------------------------------------------------
void DrawEllipse(Mat img, double angle)
{
int thickness = 2;
int lineType = 8;
ellipse( img,
Point( WINDOW_WIDTH/2, WINDOW_WIDTH/2 ),
Size( WINDOW_WIDTH/4, WINDOW_WIDTH/16 ),
angle,
0,
360,
Scalar( 255, 129, 0 ),
thickness,
lineType);
}
DrawEllipse()调用了ellipse,将椭圆画到img上,椭圆中心为( WINDOW_WIDTH/2, WINDOW_WIDTH/2 ) ,并且大小位于矩形( WINDOW_WIDTH/4, WINDOW_WIDTH/16 )。旋转角度为angle(0-360度)。颜色为Scalar(255,129,0)代表的蓝色,thickness为线宽2,lineType为8(8联通线型)
//---------------------【DrawFilledCircle()】--------------------------------
// 描述:实现了实心圆的绘制
//---------------------------------------------------------------------------
void DrawFilledCircle( Mat img, Point center )
{
int thickness = -1;
int lineType = 8;
circle(img,
center,
WINDOW_WIDTH / 32,
Scalar(0, 0, 255),
thickness,
lineType);
}
参数基本和DrawEllipse相同意义,不予赘述
//---------------------【DrawPolygon()】---------------------------------
// 描述:实现凹多边形的绘制
//
void DrawPolygon(Mat img)
{
int lineType = 8;
//创建一些点
Point rookPoints[1][20];
rookPoints[0][0] = Point(WINDOW_WIDTH / 4, 7 * WINDOW_WIDTH / 8);
rookPoints[0][1] = Point(3 * WINDOW_WIDTH / 4, 7 * WINDOW_WIDTH / 8);
rookPoints[0][2] = Point(3 * WINDOW_WIDTH / 4, 13 * WINDOW_WIDTH / 16);
rookPoints[0][3] = Point(11 * WINDOW_WIDTH / 16, 13 * WINDOW_WIDTH / 16);
rookPoints[0][4] = Point(19 * WINDOW_WIDTH / 32, 3 * WINDOW_WIDTH / 8);
rookPoints[0][5] = Point(3 * WINDOW_WIDTH / 4, 3 * WINDOW_WIDTH / 8);
rookPoints[0][6] = Point(3 * WINDOW_WIDTH / 4, 3 * WINDOW_WIDTH / 8);
rookPoints[0][7] = Point(26 * WINDOW_WIDTH / 40, WINDOW_WIDTH / 8);
rookPoints[0][8] = Point(26 * WINDOW_WIDTH / 40, WINDOW_WIDTH / 4);
rookPoints[0][9] = Point(22 * WINDOW_WIDTH / 40, WINDOW_WIDTH / 4);
rookPoints[0][10] = Point(22 * WINDOW_WIDTH / 40, WINDOW_WIDTH / 8);
rookPoints[0][11] = Point(18 * WINDOW_WIDTH / 40, WINDOW_WIDTH / 8);
rookPoints[0][12] = Point(18 * WINDOW_WIDTH / 40, WINDOW_WIDTH / 4);
rookPoints[0][13] = Point(14 * WINDOW_WIDTH / 40, WINDOW_WIDTH / 4);
rookPoints[0][14] = Point(14 * WINDOW_WIDTH / 40, WINDOW_WIDTH / 8);
rookPoints[0][15] = Point(WINDOW_WIDTH / 4, WINDOW_WIDTH / 8);
rookPoints[0][16] = Point(WINDOW_WIDTH / 4, 3 * WINDOW_WIDTH / 8);
rookPoints[0][17] = Point(13 * WINDOW_WIDTH / 32, 3 * WINDOW_WIDTH / 8);
rookPoints[0][18] = Point(5 * WINDOW_WIDTH / 16, 13 * WINDOW_WIDTH / 16);
rookPoints[0][19] = Point(WINDOW_WIDTH / 4, 13 * WINDOW_WIDTH / 16);
const Point* ppt[1] = { rookPoints[0] };
int npt[] = { 20 };
fillPoly(img,
ppt,
npt,
1,
Scalar(255, 255, 255),
lineType);
}
DrawPolygon()函数是用于绘制凹多边形的函数,区别于前两个函数的是,增加了多边形的顶点集ppt,要绘制的多边形的顶点数目为npt。要绘制的多边形的数量为1
//-------------------------【DrawLine()】-----------------------------
// 描述:绘制线
//--------------------------------------------------------------------
void DrawLine(Mat img, Point start, Point end)
{
int thickness = 2;
int lineType = 8;
line(img,
start,
end,
Scalar(0, 0, 0),
thickness,
lineType);
}
start和end是线的起点和终点。
//------------------【头文件、命名空间包含部分】--------------------
// 描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------
#include
#include
#include
#include
using namespace cv;
//----------------------【宏定义部分】----------------------------
// 描述:定义一些辅助宏
//----------------------------------------------------------------
#define WINDOW_NAME1 "【绘制图1】" //为窗口标题定义的宏
#define WINDOW_NAME2 "【绘制图2】" //为窗口标题定义的宏
#define WINDOW_WIDTH 600 //定义窗口大小的宏
//--------------------【main()】-------------------------------
// 描述:控制台应用程序的入口函数,我们的程序从这里开始执行
//---------------------------------------------------------------
int main(void)
{
//创建空白Mat以供绘图
Mat atomImg = Mat::zeros(WINDOW_WIDTH, WINDOW_WIDTH, CV_8UC3);
Mat rookImg = Mat::zeros(WINDOW_WIDTH, WINDOW_WIDTH, CV_8UC3);
//------------------<绘制化学的原子示例图>----------------
//【1.1】先绘制出椭圆
DrawEllipse(atomImg, 90);
DrawEllipse(atomImg, 0);
DrawEllipse(atomImg, 45);
DrawEllipse(atomImg, -45);
//【1.2】再绘制圆心
DrawFilledCircle(atomImg, Point(WINDOW_WIDTH / 2, WINDOW_WIDTH / 2));
//------------------------<2>绘制组合图---------------------------
//【2.1】先绘制出多边形
DrawPolygon(rookImg);
//【2.2】绘制矩形
rectangle(rookImg,
Point(0, 7 * WINDOW_WIDTH / 8),
Point(WINDOW_WIDTH, WINDOW_WIDTH),
Scalar(0, 255, 255),
-1,
8);
//【2.3】绘制一些线段
DrawLine(rookImg, Point(0, 15 * WINDOW_WIDTH / 16), Point(WINDOW_WIDTH, 15 * WINDOW_WIDTH / 16));
DrawLine(rookImg, Point(WINDOW_WIDTH / 4, 7 * WINDOW_WIDTH / 8), Point(WINDOW_WIDTH / 4, WINDOW_WIDTH));
DrawLine(rookImg, Point(WINDOW_WIDTH / 2, 7 * WINDOW_WIDTH / 8), Point(WINDOW_WIDTH / 2, WINDOW_WIDTH));
DrawLine(rookImg, Point(3 * WINDOW_WIDTH / 4, 7 * WINDOW_WIDTH / 8), Point(3 * WINDOW_WIDTH / 4, WINDOW_WIDTH));
//----------------------<3>显示绘制出的图像-------------------------
imshow(WINDOW_NAME1, atomImg);
moveWindow(WINDOW_NAME1, 0, 200);
imshow(WINDOW_NAME2, rookImg);
moveWindow(WINDOW_NAME2, WINDOW_WIDTH, 200);
waitKey(0);
return 0;
}
请留意,C++如果函数定义在main()后,需要在main之前进行声明。
运行结果图如下: