OpenCV(C++) 学习笔记(一)

新坑。学习下如何使用OpenCV。

使用教材,《OpenCV3编程入门》—毛星云 电子工业出版社。(感谢毛佬留下的宝贵知识财富!

使用的IDE。Visual Studio 2022+OpenCV 4.6.0。环境什么的安装细节很多blog都有,不多赘述。

旧坑还在继续加油,但是难度比较大,一是因为教授的飞机语速,二是由于确实内容比较难懂。只能说,菜鸟会尽力写完的。

别质疑,先相信!

一、初识OpenCV

1.5 简单的DIP程序

图像腐蚀(erode)

原理:卷积核和图像按位相乘,并滑动。将计算结果中的最小值赋予中心像素作为结果。

实现:卷积,若全部按位相乘的结果都不为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(j),作用为取出x矩阵的指向其 j 行的char类型指针

下面是对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的核。

图像膨胀(dilate)——与图像腐蚀相对的一个操作

实现:用一个结构元素(一般是3×3的大小)扫描图像中的每一个像素,用结构元素中的每一个像素与其覆盖的像素做“与”操作,如果都为0,则该像素为0,否则为1。

效果:(对二值图像)图像中黑的色块减少,白色增加。

图像模糊——均值滤波

Canny算子边缘检测

#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

1.6 OpenCV视频操作基础

读取播放视频

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——表示调用摄像头。

二、开始前的认知准备

2.1 OpenCV官方例程赏析

在OpenCV文件夹里的“.......\opencv\sources\samples\cpp\tutorial_code”有官方示例程序。其内容按照组件模块分类,适合学习。

2.1.1 彩色目标跟踪:Camshift

“.......\opencv\sources\samples\cpp\tutorial_code”中找到Camshiftdemo.cpp。复制黏贴新建一个文件就可以运行了。

2.1.2 光流:optical flow

2.1.3 点追踪:lkdemo

2.1.4 人脸识别:objectDetection

2.1.5 支持向量机引导

2.2 编译OpenCV源代码

PS:vs2017及以上的版本已经基本包含CMake的功能了,所以可以不用再下载CMake了。

而解决方案可以直接生成。(.sln文件)

2.5 argc与argv参数

        在查看OpenCV的示例程序时,经常看到int argc和char *argv [ ] 。其中arg指的是argument(参数),用于统计运行程序时送给main函数的命令行参数的个数;argv加上*和 [ ] 成为*argv[ ] 。表示字符串数组,用于存放指向字符串参数的指针数组,每个元素指向一个参数。

2.6 printf()

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图形用户界面初步

HighGUI模块是高层GUI图形用户界面模块,包含媒体输入输出、视频捕捉、图像和视频的编码解码、图形交互界面的接口等内容。第三章中我们将学习到,一些常用的交互操作,包括图像载入、显示和输出、为程序添加滑动条、鼠标操作等。

3.1 图像载入、显示和输出到文件

3.1.1 OpenCV的命名空间

        OpenCV中C++类和函数都是定义在命名空间cv之内的,有两种方法可以调用:其一,在代码#include库完毕后,添加using namespace cv;的代码;否则,在每一次调用OpenCV的类和函数时,都要在前面添加上 cv:: ,这种情况非常繁琐,很不推荐。(但是第一种方法使用之后,使用cout等std的函数和类的时候需要添加 std::

3.1.2 Mat类简析

        为了方便使用,OpenCV使用Mat类作为数据结构进行图像存取。默认大小为0。当然,在初始化时也可以指定其初始尺寸,比如定义一个Mat类对象,cv::Mat pic(320,640,cv::Scalar(100));

一般来说,我们可以用Mat srcImage = imread(“1.jpg”)来将文件名为“1.jpg”的图像文件加入到名为srcImage的Mat变量中。

3.1.4 图像载入:imread()

其原型为 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。

3.1.5 图像显示:imshow()

原型: void imshow(const string&winname,InputArray mat)

第一个参数:填写需要显示的窗口标识名。默认为CV_WINDOW_AUTOSIZE,即显示原始图像大小。否则图像会缩放以适应窗口。(缩放取决于图像深度,规则如下:

        若图像是8位无符号,显示原图

        若16位无符号或32位整型,则用像素值除以256。即将灰度值映射到【0~255】

        若是32位浮点型,用像素值乘以255。亦即映射到【0-255】

第二个参数:填需要显示的图像

3.1.6 InputArray类型

详细定义较长。大多情况,将InputArray/OutArray类型当作Mat类型即可。

3.1.7 创建窗口:namedWindow()

namedWindow函数用于创建一个窗口。如果只是简单进行图片显示,直接使用imread和imshow即可。但是如果需要在显示窗口之前就用到窗口名时(比如滑动条的使用),就需要用到namedWindow。

原型如下: void namedWindow(const string&winname,int flags-WINDOW_AUTOSIZE);

第一个参数,填写被用作窗口标识符的窗口名称。

第二个参数,是窗口标识。可以是如下几种值

        WINDOW_NORMAL,用户可以改变窗口大小

        WINDOW_AUTOSIZE(默认值),窗口大小自动适应图像大小,用户无法手动改变

        WINDOW_OPENGL,窗口创建时会支持OpenGL

namedWindow是通过指定的名字,创建一个可以作为图像和进度条的容器窗口。(如已经存在相同名称的窗口,则不执行任何操作)。同样的,我们可以使用destroyWindow()或者destroyAllWindow()来关闭窗口,并取消之前分配的与窗口相关的内存空间。

3.1.8 输出图像到文件:imwrite()

原型:bool imwrite(const string& filename,InputArray img,const vector¶ms=vector());

第一个参数,const string&类型的filename,填需要写入的文件名(带后缀),如“1.jpg”

第二个参数,InputArray类型的img,一般填一个Mat类型的图像数据(也就是待输出的图像)

第三个参数,一般无需填写,默认为vector()。如果一定要填写,按照下面的规则填

        对JPEG图片,参数表示从0-100的图片质量,默认95

        对PNG,表示压缩级别,从0-9。越高,尺寸越小,压缩时间越长

        对PPM、PGM、PBM,表示一个二进制格式标志,取0或1,默认1

3.2 滑动条的创建和使用

滑动条(Trackbar也叫做轨迹条)是OpenCV动态调节参数特别好用的一种工具,依附于窗口存在。(由于OpenCV没有按钮的功能,很多时候,可以用仅含0-1的滑动条实现按钮的按下和弹起的效果)

3.2.1 创建滑动条:createTrackbar()

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()来确保不是判断的问题。最后发现是文件名打错了。。。大家要多多检查。。。

        那么上面这个程序执行的结果就是,生成一个名为【线性混合示例】的窗口,有一个滑动条可以调节两张图片混合时各自的透明度,具体效果如下图。

 3.2.2 获取当前轨迹条的位置:getTrackbarPos()函数

        getTrackbarPos()是和createTrackbar配合使用的函数,用于获取当前轨迹条的位置。函数原型为:int getTrackbarPos( const string& trackbarname, const string& winname );

第一个参数,const string&类型的trackbarname,是轨迹条的名字

第二个参数,const string&类型的winname,表示轨迹条所属的窗口名称

3.3 鼠标操作

        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)));
	//随机颜色
}

        最终实现效果如下图 

OpenCV(C++) 学习笔记(一)_第1张图片

        简单来说,我们可以通过按下鼠标左键和松开来在窗口中绘制彩色的矩形。而程序中的on_MouseHandle就是回调函数。其中一个switch语句定义了对于不同的鼠标事件进行对应的操作。

四、OpenCV数据结构与基本绘图

4.1、基础图像容器Mat(位图)

4.1.2 Mat结构使用

        对于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()复制一幅图像的矩阵

4.1.3 像素值的存储方法

        存储像素值需要指定颜色空间和数据类型。其中,颜色空间是指针对一个给定的颜色,如何组合颜色元素以对其编码。最简单的颜色空间是灰度级空间,只处理黑色和白色。

        对于彩色,则有更多种类的颜色空间。但都是将颜色分成三或四个基本元素。RGB颜色空间就是最常用的颜色空间。基色是R(红)、G(绿)、B(蓝),有时为了表示透明颜色有第四个元素alpha(A)。每个组成元素都有其定义域(取决于其数据类型)。最小的是char,占一个字节或者8位,可以是有符号型(0~255)或者无符号(uchar,-128~+127)。

4.1.4 显式创建Mat对象的七种方法

        在第一章里,我们学习过imwrite()将一个矩阵(图像)写入到图像文件中。但是如果是debug,观看实际值会更加方便,我们可以通过Mat的运算符 “ << ” 实现,但是,“ << ”只对二维矩阵有效。

        Mat不但是一个非常有用的图像容器类,同时也是一个通用的矩阵类,我们也可以用它创建和操作多维矩阵。创建方法有多种,如下:

【方法一】使用Mat()构造函数

最常用的方法是直接用Mat构造函数()。

Mat M(2,2, CV_8UC3, Scalar(0,0,255));
cout<<"M = "<

程序运行结果如下:

OpenCV(C++) 学习笔记(一)_第2张图片

对于输出结果,是对于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 = " <

4.1.5 OpenCV中的格式化输出方法

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;
}

OpenCV(C++) 学习笔记(一)_第3张图片

OpenCV(C++) 学习笔记(一)_第4张图片

OpenCV(C++) 学习笔记(一)_第5张图片

4.1.6 输出其他常用数据结构

 除了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;
}

OpenCV(C++) 学习笔记(一)_第6张图片

4.2 常用数据结构和函数

4.2.1 点的表示:Point类

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_、Point2i、Point互相等价,Point_、Point2f互相等价。

4.2.2 颜色的表示:Scalar类

        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_。这就解释了为什么许多函数的参数输入可以是Mat,也可以是Scalar。

4.2.3 尺寸的表示:Size类

        通过在代码中对Siz类进行“转到定义”操作,我们可以在........\opencv\sources\modules\core\include\opencv2\core\core.hpp路径下,找到Size类的相关源代码(注意,由于OpenCV版本不同,路径可能稍有不同,但是core.hpp的文件名应当还是未改变的)

typedef Size_ Size2i;
typedef Size2i Size;

        其中,Size_是个模板类,Size_表示其内部模板所代表的类型为int。那么上面两行代码就是首先给已知的Size_起个新名字——Size2i。然后又给Size2i起个新名字——Size(所以,Size_、Size2i、Size等价)

        追根溯源到Size_模板类的定义,可以发现内部重载了一些构造函数,其中使用频率最高的是下面这个

Size_(_Tp _width,_Tp _height)
_Tp width,height;//宽度和高度

//例子:构造出的Size宽度高度都为5
Size(5,5)

4.2.4 矩形的表示:Rect类

        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;

4.2.5 颜色空间转换:cvtColor()函数

        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();
}

4.2.6 其他常用知识点

本小节列举一些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.中一些函数使用起来很方便,有计算向量角度的函数fastAtan2、计算立方根的函数cubeRoot、向上取整函数cvCeil、向下取整函数cvFloor、四舍五入函数cvRound等。还有一些类似Matlab里的函数,比如cvIsInf判断自变量是否无穷大,cvIsNaN判断自变量是否是个数

6.显示文字相关的函数:grtTextSize、cvInitFont、putText

7.作图相关的函数:circle、clipLine、ellipse、ellipse2Poly、line、rectangle、polylines、类LineIterator

8.填充相关的函数:fillConvexPoly、fillPoly

9.OpenCV中RNG()函数能够为初始化随机数状态的生成器

4.3 基本图形绘制

        本节主要讲述使用point在图像中定义2D点、如何使用Scalar表示颜色的值。涉及到以下函数:

·绘制直线——line

·绘制椭圆——ellipse

·绘制矩形——rectangle

·绘制圆——circle

·绘制填充多边形——fillPoly

4.3.1 DrawEllipse()的写法

#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联通线型)

4.3.2 DrawFilledCircle()

//---------------------【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相同意义,不予赘述

4.3.3 DrawPolygon()

//---------------------【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

4.3.4 DrawLine()

//-------------------------【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是线的起点和终点。

4.3.5 main()

//------------------【头文件、命名空间包含部分】--------------------

//            描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------
#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之前进行声明。

        运行结果图如下:

OpenCV(C++) 学习笔记(一)_第7张图片

你可能感兴趣的:(学习,笔记,c++)