从零开始:数字图像处理的编程基础与应用

978-7-115-57955-3
作者: 彭凌西彭绍湖唐春明陈统
译者:
编辑: 张天怡

图书目录:

详情

本书主要介绍数字图像处理基础知识与基于OpenCV和C++的图像编程技术的相关内容,旨在帮助读者尽快掌握数字图像理论知识和编程技术。 本书第1章主要介绍OpenCV基础;第2章主要介绍图像预处理;第3章主要介绍图像分割和数学形态学;第4章主要介绍特征提取与匹配;第5章主要介绍模板匹配与轮廓绘制;第6章主要介绍视频录制与目标追踪;第7章主要介绍三维重建;第8章主要介绍距离测量与角点检测;第9章主要介绍图像识别应用,涉及文字识别、二维码识别、人脸识别和车牌识别等内容;第10章主要介绍基于深度学习的图像应用。 书中通过近百个编程实例和项目,帮助读者掌握数字图像处理原理,并进一步掌握数字图像的编程技术。 本书不仅适合各类院校相关专业的学生使用,也适合对数字图像编程感兴趣,已有一定的C++编程基础,但没有数字图像基础理论知识的读者阅读。

图书摘要

版权信息

书名:从零开始:数字图像处理的编程基础与应用

ISBN:978-7-115-57955-3

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。


版  权

著    彭凌西  彭绍湖  唐春明  陈 统

责任编辑 张天怡

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315

内 容 提 要

本书主要介绍数字图像处理基础知识与基于OpenCV和C++的图像编程技术的相关内容,旨在帮助读者尽快掌握数字图像理论知识和编程技术。

本书第1章主要介绍OpenCV基础;第2章主要介绍图像预处理;第3章主要介绍图像分割和数学形态学;第4章主要介绍特征提取与匹配;第5章主要介绍模板匹配与轮廓绘制;第6章主要介绍视频录制与目标追踪;第7章主要介绍三维重建;第8章主要介绍距离测量与角点检测;第9章主要介绍图像识别应用,涉及文字识别、二维码识别、人脸识别和车牌识别等内容;第10章主要介绍基于深度学习的图像应用。

书中通过近百个编程实例和项目,帮助读者掌握数字图像处理原理,并进一步掌握数字图像的编程技术。

本书不仅适合各类院校相关专业的学生使用,也适合对数字图像编程感兴趣,已有一定的C++编程基础,但没有数字图像基础理论知识的读者阅读。

“数字图像处理”是一门理论和实践紧密结合的课程,主要介绍通过计算机对数字图像去除噪声、增强、复原、分割和特征提取等处理原理、方法和技术。在该课程教学过程中,针对图像处理应用性较强的特点,可利用所学到的理论知识来解决实际的图像问题。

国务院在2017年7月印发了《新一代人工智能发展规划》,明确提出实施全民智能教育项目,逐步推广编程教育。“数字图像处理”课程具有教学内容较多、理论性偏强、缺乏数学基础难以理解和掌握、内容更新速度快、应用性强等特点,如何简明扼要、通俗易懂地讲解数字图像处理原理和技术,并与通用OpenCV库和C++结合,让读者高效、快速掌握数字图像处理编程技术的书籍和资料甚少。

本书较好地结合了编者在高校与企业多年的教学和研究经验,深入浅出地介绍了OpenCV基础、图像预处理、图像分割和数学形态学等内容,剖析数字图像处理在识别、视频录制与目标追踪等方面的典型案例,并给出全部已编译运行的源码、课件、大纲等资源,读者可事半功倍地掌握数字图像处理原理与基于OpenCV和C++的图像编程技术。

相信本书的出版会对想尽快掌握数字图像处理原理与基于OpenCV和C++的图像编程技术的读者与研究人员大有裨益,对编程教育和人工智能的发展起到很大的促进作用。同时也希望能够有更多的研究人员掌握数字图像编程技术,从事人工智能研究和教育工作,为推动我国新一代人工智能创新活动的蓬勃发展做出贡献。

教授 中国科学院院士

2022年1月

前   言

近年来,随着计算机技术和数学等基础学科的发展,以及军事、工业和医学等行业应用需求的急剧增长,数字图像处理(Digital Image Processing)技术得到了迅猛发展,已深入渗透到人类生活的各个领域,并得到越来越多的应用。

“数字图像处理”是一门理论和实践紧密结合的课程,具有很强的理论性和实用性,学生可掌握图像处理的理论、方法以及具体的应用技术。但是,“数字图像处理”课程涉及小波变换、高斯滤波、图像分割、图像特征提取等晦涩难懂的理论和概念,让初学者望而却步。另外,能简明扼要、通俗易懂地介绍数字图像处理基本原理和技术,并与数字图像处理领域首选的C++编程语言结合,让读者快速高效掌握数字图像处理编程方法的书籍和资料甚少。而本书采用数字图像处理中最为通用的OpenCV库,结合C++语言,对“数字图像处理”课程涉及的主要内容进行了通俗易懂和全面的讲解。与已有的图像处理和计算机视觉编程教材或书籍相比,本书具有以下特色。

通俗易懂,深入浅出。

本书通过近百个编程实例和项目,详细的代码注释讲解和结果分析,简洁精练的语言,通俗易懂地介绍了数字图像处理领域的经典理论和算法,让难以理解的知识能够轻松被读者掌握,且内容深入浅出,让读者既能学习基础理论,又能提高应用所学知识分析解决问题的能力。本书的初稿完成后还请多位数字图像处理专家进行了审阅,多位教师试用,很多学生进行了编程实践操作。该书历经多年教学实践,反复修改,使其内容易懂、易教,可谓数年磨一剑。如果读者难以看懂书中的图像处理原理,建议先看图像处理效果。

重点突出,循序渐进。

本书按照数字图像处理工程技术的编程思路,从OpenCV编程环境搭建入手,依次详细讲解了OpenCV基础,图像预处理,图像分割和数学形态学处理,最后对数字图像处理重点应用工程领域,如视频录制与目标追踪,三维重建,距离测量与角点检测,文字识别、二维码识别、人脸识别和车牌识别,基于深度学习的图像应用等进行了详细介绍。部分例子是研发实例的精简,这些例子没有一味追求实用和全面,而是重点讲解基本原理和操作,并添加了详尽的代码注释,以便读者快速掌握,同时又注意了可维护性和扩充性,可快速扩展到具体工程应用。

实例丰富,快速上手。

书中在OpenCV基础部分,提供了9个编程实例;在图像预处理部分,提供了18个编程实例;在图像分割和数学形态学部分,提供了22个编程实例;在特征提取与匹配部分,提供了17个编程实例;在模板匹配与轮廓绘制部分,提供了3个编程实例;在视频录制与目标追踪部分,提供了4个编程实例;在三维重建部分,提供了8个编程实例;在距离测量与角点检测部分,提供了5个编程实例;在图像识别应用部分,提供了文字识别、二维码识别、人脸识别和车牌识别等8个编程实例;在基于深度学习的图像应用部分,提供了3个编程实例。这些实例包含数字图像处理的基本原理和算法,也涉及数字图像处理的各个应用技术。

资源丰富,易学易教。

本书QQ群(764353211)群文件提供了在Qt 5.9 或VS 2022编程环境上编译通过的全部C++示例源码以及配套课件和大纲等资源。

本书第1、6章主要由梁志炜完成,第2章主要由李动员完成,第3章主要由彭绍湖和李动员完成,第4章主要由张一梵完成,第5章主要由彭凌西和唐春明完成,第7章主要由肖鸿鑫完成,第8、10章主要由关喜荣完成,第9章主要由黄明龙、关喜荣完成,附录主要由陈统完成。彭凌西和关喜荣还对所有章节进行了修改,对部分章节进行了内容扩充,为本书做了较大贡献。

本书在编写过程中,得到了很多专家、教师、企业人员和学生的大力支持与帮助。胡晓、肖忠、林锦辉、柯子颜、罗雪冰等众多老师和学生对全书进行了试读与校稿,并提出许多宝贵的意见,让本书通俗易懂,从而达到提高学习效率的效果。他们认真、细致的工作让我感动。本书还得到数据恢复四川省重点实验室、广州大学研究生优秀教材建设项目和教务处的大力支持,受到国家自然科学基金项目(12171114、61772147和61100150)、广东省自然科学基金基础研究重大培育项目(2015A030308016)、国家密码管理局“十三五”国家密码发展基金项目(MMJJ20170117)、广州市教育局协同创新重大项目(1201610005)、密码科学技术国家重点实验室开放课题项目(MMKFKT201913)的资助,并得到深圳市创科视觉技术有限公司、深圳越疆科技有限公司、广东轩辕网络科技股份有限公司和广州粤嵌通信科技股份有限公司等的竭诚帮助。

读者有任何意见或反馈,请联系关喜荣:836030680@qq.com,彭凌西:flyingday@139.com。

感谢可爱的女儿,你们的天真和可爱让一切忧愁与烦恼烟消云散。最后与读者分享编者在多年计算机教学、研究过程中的三点体会。

彭凌西

2022年1月于广州大学城

第1章 OpenCV基础

OpenCV是一个跨平台的计算机视觉和机器学习软件库,可运行在Linux、Windows、Android和macOS等操作系统上,虽然是轻量级的,但特别高效。它由一系列C语言函数和少量C++类构成,目前实现了很多图像处理和计算机视觉方面的通用算法,并且提供了C语言、C++、Python、Ruby、MATLAB等多种语言的接口。由于OpenCV的应用领域非常广泛,例如人机互动、图像分割识别、运动分析追踪、机器视觉等,所以它一直都很受欢迎,被广泛运用于各行各业,从互动艺术、矿山检查、网络地图到先进的机器人技术都有OpenCV的身影。

本章主要内容和学习目标如下。

1.1 OpenCV简介

OpenCV(Open Source Computer Vision Library)是一个开源的计算机视觉和机器学习软件库。OpenCV旨在为计算机视觉应用提供通用基础设施,并加速商业产品中机器感知技术的使用。作为伯克利软件套件(Berkeley Software Distribution,BSD)许可产品,OpenCV允许企业利用和修改其代码。该库拥有超过2500种优化算法,其中包括一套全面的经典算法,以及十分先进的计算机视觉和机器学习算法。这些算法可用于检测和识别面部,识别物体,对视频中的人体动作进行分类,追踪相机移动,追踪移动物体,提取物体的3D模型,从立体相机生成3D点云,将图像拼接在一起以产生高分辨率的整个场景图像,从图像数据库中找到相似的图像,从使用闪光灯拍摄的图像中移除红眼,追踪眼睛运动,识别风景并建立标记以用增强现实覆盖等。

OpenCV具有C++、Python、Java和MATLAB等多种语言接口,并支持Windows、Linux、Android和macOS等操作系统。OpenCV主要倾向于实时视觉应用。OpenCV本身是用C/C++编写的,具有模板化的接口,可以与标准模板库(Standard Template Library,STL)容器无缝协作。

1.2 OpenCV 编程环境搭建

本书使用的编程语言为C++,编程环境可为Visual Studio或Qt,读者可根据自己的偏好搭建相应的编程环境,例如使用“Visual Studio 2022 + OpenCV 4.4”,或者“Qt 5.14.2+ OpenCV 4.4”。下面将详细介绍软件环境搭建方法。

· 1.2.1 Visual Studio 2022安装

Visual Studio 2022(VS 2022)安装包可直接在Visual Studio官方网站,选择免费的社区版(Community 2022)下载,如图1-1所示。

图1-1 VS 2022下载页面

下载VS 2022安装包后,双击打开安装包开始安装,安装页面如图1-2所示。

图1-2 VS 2022安装页面

单击“继续”按钮,安装组件选择“使用C++的桌面开发”,如图1-3所示。在“安装位置”下选择VS 2022安装路径为D:\VS,并对下载缓存进行设置,如图1-4所示。

图1-3 开发组件的选择

图1-4 VS 2022安装位置

单击“安装”按钮后,如图1-5所示,下载时间较长,请耐心等待,下载安装完成后重启电脑即可使用VS 2022。

图1-5 开始下载安装

· 1.2.2 Qt安装

Qt(官方发音[kju:t],音同cute)是一个跨平台的C++开发库,主要用来开发图形用户界面(Graphical User Interface,GUI)程序,当然也可以开发不带界面的命令行用户交互(Command User Interface,CUI)程序。本书将使用Qt来编写程序。本书使用的Qt版本号为5.14.2,系统环境为Windows 10。

打开Qt官网,进入下载界面,选择5.14.2版本后,进入下载地址界面,找到“qt-opensource-windows-x86-5.14.2.exe”安装包下载即可,如图1-6所示。

图1-6 下载Qt

安装过程如图1-7所示,本书中Qt的安装目录为D:\QT,Qt代码存放路径为D:\QTCode(例如第1章的代码,Qt代码路径为D:\QTCode\1),图像路径统一为D:\images。

图1-7 安装Qt

需在Qt网站注册账号并登录,此处不展示。注册完毕单击“Next”按钮进入下一步,如图1-8所示。

图1-8 选择“下一步”

勾选“I have read and approve the obligations of using Open Source Qt”后,单击“下一步”进入安装程序,选择具体的安装目录,继续单击“下一步”按钮,如图1-9 所示。

图1-9 自定义安装目录

选择要安装的组件(64位计算机建议勾选64位,非32位),如图1-10所示。

图1-10 选择安装组件

选择安装组件后,默认安装工具,如图1-11所示,建议在图1-10和图1-11中同时勾选MinGW编译器(注意根据计算机操作系统选64位或者32位)。

图1-11 默认安装工具

默认安装工具后,同意安装协议,如图1-12所示。

图1-12 同意许可协议

单击“下一步”按钮开始安装,安装完成后如图1-13所示。Qt Creator启动页面如图1-14所示。

图1-13 Qt安装完成

图1-14 Qt Creator启动界面

Qt在统信Unity Operating System(UOS)上面的安装比较简单,在统信UOS的桌面单击鼠标右键并选择“在终端中打开”,打开UOS的命令行终端,在命令行终端输入如下安装命令即可完成Qt的安装。

$ sudo apt-get install qt5-default qtcreator

输入命令后,sudo(类似于Windows的添加/删除程序)自动开始从网络下载所需的包,例如开发工具Qt Creator,编译器QMake,帮助文档,开发样例,等等,下载时输入字母‘y’确认下载即可。

· 1.2.3 OpenCV Release版本安装

OpenCV 4.4安装包可直接在官网下载。OpenCV包括Release(发行)版和Debug(调试)版,对于初学者来说,直接下载源码编译比较困难,可以直接下载Release版配置后直接运行。本书附录2给出了OpenCV 4.4源码和opencv_contrib模块的编译配置过程。

这里首先介绍Release版的安装配置过程。下载Release版后(约203MB,安装包名为opencv-4.4.0-vc14_vc15.exe),双击打开安装包,指定解压缩目录为D:\OpenCV,如图1-15所示。

图1-15 指定解压缩目录

· 1.2.4 VS 2022中OpenCV 4.4环境配置

在VS 2022中配置OpenCV 4.4的具体过程如下。

(1)右键单击“此电脑”图标,选择“属性”命令进入系统窗口。选择左侧的“高级系统设置”选项,打开“系统属性”对话框,选择“高级”选项卡,然后在最下方单击“环境变量”按钮。在系统变量 Path 中添加以下路径。

D:\OpenCV\opencv\build\x64\vc15\bin

注意在前面变量前先添加“;”,以便与另外一个路径分开。

(2)设置VS环境变量。

新建VS空项目“HelloCV”,并设置项目为Debug x64模式(若需设置Release模式,则设置项目为Release x64,建议设置两种模式),如图1-16所示。

图1-16 设置Debug x64模式

(3)右侧添加属性表。

单击菜单栏的“视图”菜单,然后选择“其他窗口”命令,选择“属性管理器”子命令,打开“属性管理器”面板。在“属性管理器”面板的 “Debug | x64 ”中添加新项目属性表,命名为“OpenCV440Debug”,如图1-17所示。

图1-17 添加属性表

(4)编辑属性表。

在OpenCV440Debug属性表上单击鼠标右键,在弹出的快捷菜单中选择“属性”命令,对该属性进行编辑,具体步骤如下。

①选择左侧“通用属性”目录下的“VC++目录”选项,在右侧“常规”列表中选择“包含目录”选项,在弹出的“包含目录”对话框总单击“添加”按钮,添加以下目录,然后单击“确定”按钮,如图1-18所示。

D:\OpenCV\opencv\build\include

图1-18 添加“包含目录”

②在“常规”列表中选择“库目录”选项,在弹出的“库目录”对话框中添加以下目录,然后单击“确定”按钮,如图1-19所示。

D:\OpenCV\opencv\build\x64\vc15\lib

图1-19 添加“库目录”

③选择左侧“通用属性”目录下的“链接器”选项,选择其下方的“输入”子选项,在右侧“附加依赖项”中添加以下文件,如图1-20所示。

opencv_world440d.lib

图1-20 添加“附加依赖项”

opencv_world440d.lib文件名中的最后一个字母“d”表示VS生成解决方案为Debug模式。如果为VS中配置Release模式,需要去掉该字母。

④确认更改,退出属性编辑器。

⑤进行测试(此处代码暂时先不予解释,将在1.3节介绍)。

编辑测试代码源.cpp文件,内容如下。

#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>
using namespace std;
using namespace cv;
int main(int argc, char** argv)
{
    String imageName(“touxiang.jpg”); // 默认文件
    if (argc > 1)
        imageName = argv[1];
    Mat image = imread(samples::findFile(imageName), IMREAD_COLOR); // 读取文件
    if (image.empty()) {  // 检查无效输入
        cout << “Could not open or find the image” << endl;
        return -1;//依次处理
    }
    namedWindow(“Display window”, WINDOW_AUTOSIZE); //新建一个显示窗口,默认图像大小
    imshow(“Display window”, image);                // 在窗口中显示图像
    waitKey(0);   // 无限等待,直到有按键被按下结束
    return 0;
}

注意:将图像文件touxiang.jpg放到源.cpp文件所在的目录下,程序运行结果如图1-21所示。

图1-21 程序运行结果

那么,是否每次新建项目都需要配置属性表?其实不需要,有以下两种方法可以使用。

方法一:将此项目作为一个模板,以后新建项目都复制此项目,在其基础上进行编辑。

方法二:此项目创建好之后,会在项目根目录下形成OpenCV440Debug.props文件,将此文件复制出来,放到一个固定目录下,以后新建项目时在属性表里选择“添加现有属性表”按钮,导入该文件即可。

· 1.2.5 Qt 5.14.2中OpenCV 4.4环境搭建

编译Qt程序常用的有MinGW和MSCV编译器。由于MinGW编译器需要用CMake重新编译源码,操作起来较为烦琐,因此本书采用MSCV编译器,且通过配置Qt工程中的.pro文件添加OpenCV的库文件和头文件。搭建过程如下。

(1)打开Qt Creator,新建C++工程项目,如图1-22所示。

图1-22 新建C++项目

(2)设置项目名称和路径,如图1-23所示。

图1-23 项目名称、路径

(3)选择编译器。这里可选的编译器有QMake、CMake等,默认使用Qt自带的QMake编译工具。

(4)工程配套选择“Desktop Qt 5.14.2 MSVC2015 64bit”(与图1-10选择的编译器对应),如图1-24所示。

图1-24 工程配套选择

(5)不需要配置工程管理则此处保持默认配置,单击“完成”按钮,如图1-25所示。

图-1-25 工程管理

(6)项目创建完成后可以看到项目目录和文件,如图1-26所示。

图1-26 项目目录

第一次构建Qt项目时可能会出现文件不属于项目的警告提示,如图1-27所示。

图1-27 警告提示

单击左侧的项目,配置项目模式,如图1-28所示,进行项目模式后,可以看到项目配置窗口,如图1-29所示。

图1-28 配置项目模式

图1-29 项目配置窗口

单击“Manage... ”按钮,配置编译工具(与图1-10中对应),如图1-30所示。

图1-30 配置编译工具

按图1-31所示,务必在这一步选择组件,否则后续构建时会出现错误,注意X86(代表32位)与64位计算机不兼容。

选择正确的C ++编译器Microsoft Visual C++ Compiler 17.2.32616.157(amd64)即可,不需要配置C语言编译器(本书使用C++进行开发),如图1-32所示。

图1-31 kit配置

图1-32 配置C++编译器

至此,问题已经解决。

(7)双击OpenCVTest.pro文件并编辑,添加如下Opencv配置。

添加OpenCV的头文件和库文件,头文件只需要添加所在路径即可,库文件则需要提供库文件的所在路径,以及编译的时候链接到哪一个库文件,如图1-33所示。

图1-33 .pro文件配置OpenCV环境

# 导入头文件
INCLUDEPATH+=D:/OpenCV/opencv/build/include
INCLUDEPATH+=D:/opencv/opencv/build/include/opencv2
# 导入库文件
win32:CONFIG(debug, debug|release): {
LIBS+=-LD:/OpenCV/opencv/build/x64/vc15/lib\
-lopencv_world440d
}
else{
LIBS+=-LD:/OpenCV/opencv/build/x64/vc15/lib\
-lopencv_world440
}

(8)编写测试程序main.cpp文件,具体内容如下。

#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>
using namespace std;
using namespace cv;
int main(int argc, char** argv)
{
    String imageName(“D:/images/touxiang.jpg”); // 默认文件路径
    if (argc > 1)
        imageName = argv[1];
    Mat image = imread(samples::findFile(imageName), IMREAD_COLOR); //读取文件
    if (image.empty()) {  // 检查输入文件
        cout << “Could not open or find the image” << endl;
        return -1;
    }
    namedWindow(“Display window”, WINDOW_AUTOSIZE); // 创建窗体显示
    imshow(“Display window”, image);                // 显示图像
    waitKey(0);   // 等待键盘事件
 
    return 0;
}

注意:图像路径统一为D:\images,在该目录下放置图像touxiang.jpg,如图1-34所示。

图1-34 图像放置路径

(9)对工程进行QMake编译,将鼠标指针移动到左侧的工程名上并单击鼠标右键,在弹出的快捷菜单中选择“qmake”命令进行QMake编译。QMake编译根据实际环境创建项目文件.pro,生成适当的Makefile文件。这个步骤如果不执行,后面无法构建编译运行。

如果状态栏下方的“编译输出”窗口没有出现异常错误提示,则表示编译通过,如图1-35所示。

图1-35 编译程序

(10)QMake编译通过之后,将鼠标指针移动到左侧的工程名上单击鼠标右键,在弹出的快捷菜单中选择“构建”命令。同样,此时下方的“编译输出”窗口没有出现异常错误提示,则表示编译通过,此时才生成可执行的.exe文件,如图1-36所示。

图1-36 构建项目

(11)编译生成.exe文件之后,可以单击Qt Creator窗口左下方的三角形按钮,运行编译通过的测试程序,如图1-37所示。

图1-37 运行程序

(12)程序运行结果如图1-38所示。

图1-38 程序运行结果

在Qt中编译运行的顺序为先执行QMake,然后构建,最后运行。

这里需要特别注意的一点是,OpenCV基础模块的Release版安装完成后,可以满足大多数初学人员的正常使用需要,但有很多非常实用的功能并没有被集成在基础模块中,而是被放在被称为opencv_contrib的扩展模块中,例如人脸识别、生物视觉、特征点提取等众多非常强大的功能。扩展模块是对基础模块功能的补充,欲了解关于OpenCV 4.4源码和opencv_contrib模块的编译和安装,请参考本书附录2。

1.3 Mat图像存储容器

人们从现实世界中获取数字图像的途径有数码相机、扫描仪、计算机断层扫描和核磁共振成像等。在任何情况下,人眼所看到的都是图像。然而,实际上数字设备记录的是图像中每个点的数值,如图1-39所示。

图1-39 图像表示

可以看到,在上述图像中,图像的成像只不过是一个包含所有像素点强度值的矩阵。获取和存储像素值的方法可能会根据不同的需要而有所不同,但最终,计算机世界里的所有图像都可以简化为数值矩阵信息。OpenCV是一个计算机视觉库,可以帮助我们处理和操纵这些信息。因此,读者需要熟悉的第一件事是OpenCV如何存储和处理图像。

· 1.3.1 Mat容器简介

Mat容器是早期(2001年)OpenCV基于C语言接口建立的。为了在内存中存放图像,当时采用的是IplImage C语言结构体。然而,采用这种方法用户必须接受C语言所有的不足,其中最大的不足是需要手动进行内存管理,即用户需要对开辟和销毁内存负责。虽然对于小型的程序来说手动管理内存不是问题,但一旦代码变得庞大,用户就会越来越多地纠结于这个问题,而不是着力实现其开发目标。

幸运的是,C++出现了,并且带来了类的概念。同时,由于C++与C语言完全兼容,所以在进行代码更改时不会出现任何兼容性问题。为此,OpenCV在2.0版本中引入了一个新的C++接口,并在OpenCV中使用了Mat类,通过Mat解决了内存管理和运算符重载等问题,利用自动内存管理给出了解决问题的新方法。使用这个方法,用户不需要纠结于如何管理内存,而且代码会变得简洁。C++接口唯一的不足是当前许多嵌入式开发系统只支持C语言,所以当用户的目标不是这种开发平台(嵌入式)时,没有必要使用“旧”方法。

对于Mat类,首先要知道的是用户不必再手动为其开辟空间,也不必在不需要时立即将空间释放(但手动做这些工作还是可以的,大多数OpenCV函数仍会手动为输出数据开辟空间)。当传递一个已经存在的Mat对象时,开辟好的矩阵空间会被重用。也就是说,用户每次都使用大小正好的内存来完成任务。

Mat类由两个数据部分组成:矩阵头(包含矩阵尺寸、存储方法、存储地址等信息)和一个指向存储所有像素值矩阵(根据所选存储方法的不同,矩阵可以是不同的维数)的指针。矩阵头的尺寸是常数值,但矩阵本身的尺寸会因图像的不同而不同,通常比矩阵头的尺寸大好几个数量级。因此,当在程序中传递图像并创建副本时,大的开销是由矩阵造成的,而不是矩阵头。OpenCV是一个图像处理库,其中包含大量的图像处理函数。为了解决问题,通常要使用库中的多个函数,因此经常需要在函数中传递图像。同时,对于那些计算量很大的图像处理算法,除非万不得已,否则不应该复制“大”图像,因为这会降低程序运行速度。

为了解决这个问题,OpenCV使用引用计数机制,其思路是让每个Mat对象有自己的矩阵头,但共享同一个矩阵(通过让矩阵指针指向同一地址来实现)。当使用复制构造函数(又称拷贝构造函数)时,只需复制矩阵头和矩阵指针即可,而不用复制矩阵本身,代码如下。

Mat A,C;                                 // 只创建矩阵头部分
A = imread(argv[1],CV_LOAD_IMAGE_COLOR); // 这里为矩阵开辟内存
Mat B(A);                                 // 使用复制构造函数
C = A;                                    // 赋值运算符

以上代码中的所有Mat对象最终都指向同一个,也是唯一的数据矩阵。虽然它们的矩阵头不同,但通过任何一个对象所做的改变都会影响其他对象。实际上,不同的对象只是访问相同数据的不同途径而已。这里还要提及一个强大的功能:可以创建只引用部分数据的矩阵头。例如用户想要创建一个感兴趣区域(Region of Interest,ROI),只需要创建包含边界信息的矩阵头即可。

Mat D (A,Rect(10,10,100,100) ); // 使用矩阵确定感兴趣区域
Mat E = A(Range:all(),Range(1,3)); // 使用行和列确定边界,这里为A矩阵全部行的第1—3列

如果矩阵属于多个Mat对象,那么当不再需要它时谁来负责清理?简单的回答是最后一个使用它的对象。清理工作可以通过引用计数机制来实现。无论用户什么时候复制一个Mat对象的矩阵头,都会增加矩阵的引用次数;反之,当一个矩阵头被释放之后,这个计数被减1;当计数值为0,矩阵就会被清理。但在某些时候确实需要完全复制矩阵本身(不只是矩阵头和矩阵指针),这时可以使用函数clone()或者copyTo()实现深复制。示例如下。

Mat F = A.clone();
Mat G;
A.copyTo(G);

此时改变F或者G就不会影响Mat矩阵头所指向的矩阵。二者的区别在于,copyTo()是否申请新的内存空间,取决于目标头像矩阵头中的大小信息是否与源图像一致,若一致则是浅复制,不申请新的空间,否则先申请空间后再进行复制;clone ()是完全的深复制,在内存中申请新的空间。

以上内容可以总结如下。

①OpenCV函数中输出图像的内存分配是自动完成的(如果不特别指定的话)。

②使用OpenCV的Mat类时不需要考虑内存释放问题。

③赋值运算符和复制构造函数只复制矩阵头。

④函数clone()或者copyTo()可用来复制表示图像的矩阵。

· 1.3.2 存储方法

这一小节讲述如何存储像素值。存储像素值需要指定颜色空间和数据类型。颜色空间是指对一个给定的颜色,如何组合颜色元素以对其编码。最简单的颜色空间要属灰度级空间,只处理黑色和白色,对它们进行组合可以产生不同程度的灰色。

对于彩色,则有更多种类的颜色空间。但不论哪种存储方法,都是把颜色分成3个或者4个基元素,通过组合基元素来产生所有的颜色。RGB颜色空间是最常用的颜色空间之一,它也是人眼内部构成颜色的方式。它的基色是红色、绿色和蓝色,有时为了表示透明颜色也会加入第四个元素alpha (A)。

不同的颜色空间,各有自身的优势,具体介绍如下。

每个组成元素都有自己的定义域,具体取决于其数据类型。如何存储一个元素决定了在该元素定义域上能够控制的精度。最小的数据类型是char,占1字节或者8个二进制位,可以是有符号型(0到255之间)或无符号型(-127到+127之间)。尽管使用3个字符char型元素已经可以表示1600万种可能的颜色(使用RGB颜色空间),但若使用单精度浮点数float(4字节,32位)型或双精度浮点数double(8字节,64位)型元素则能分辨出更加精细的颜色。但增加元素的尺寸也会增加图像所占的内存空间。

OpenCV中图像的通道数可以是1、2、3或4。其中常见的是单通道和三通道,二通道和四通道不常见。

①单通道的是灰度图像。

②三通道的是彩色图像,例如RGB图像。

③四通道的图像是RGBA图像,是RGB加上一个A通道,也叫alpha通道,表示透明度。PNG图像是一种典型的四通道图像。alpha通道可以赋值0到1,或者0到255的数字,表示从透明到不透明。

④二通道的图像是RGB555和RGB565格式的图像。二通道图像在程序处理中会用到,如傅里叶变换。其中,RGB565是16位的,只需要2字节存储每个像素点,其中第一字节的前5位是R(红色),第一字节后3位+第二字节前3位是G(绿色),第二字节后5位是B(蓝色),相对3个字节,对源图像进行了压缩。

· 1.3.3 创建Mat对象

Mat不仅是很优秀的图像容器类,同时也是通用的矩阵类,可以用来创建和操作多维矩阵。创建一个Mat对象有多种方法,此处将通过项目1-1来介绍这一部分内容。

创建Mat对象的具体操作过程如下。

(1)创建Qt项目1-1,如图1-40、图1-41所示。

图1-40 创建Qt项目1-1

图1-41 选择对应的Kits组件——MSVC2015 32bit

(2)修改1-1.pro文件来配置OpenCV环境。往1-1.pro文件中加入如下代码。

# 导入头文件
INCLUDEPATH+=D:/OpenCV/opencv/build/include
INCLUDEPATH+=D:/opencv/opencv/build/include/opencv2
# 导入库文件
win32:CONFIG(debug, debug|release): {
LIBS+=-LD:/OpenCV/opencv/build/x64/vc14/lib\
-lopencv_world440d
}
else{
LIBS+=-LD:/OpenCV/opencv/build/x64/vc14/lib\
-lopencv_world440
}

至此,环境已经配置完毕。本书后面的环境如无特殊说明均默认使用此处的环境配置方法,不再重复说明,请读者注意。

1.采用Mat()构造函数创建Mat对象

例1-1:使用Mat()构造函数创建Mat对象。

(1)编辑main.cpp文件。

#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
//创建一个类型为8位uchar、颜色为三通道黄色的2×2 Mat对象
Mat img(2,2,CV_8UC3,Scalar(0,255,255));
cout <<"矩阵元素" << endl << img << endl;
return 0;
}

(2)运行代码,步骤如下。本书后面的运行代码方法如无特殊说明均默认为此处的运行方法,不再重复说明,请读者注意。

(3)编译程序前,需要先对工程进行QMake编译。将鼠标指针移动到左侧的工程名上并右击,在弹出的快捷菜单中选择“qmake”命令进行QMake编译,结果如图1-42所示。

图1-42 QMake编译结果输出

(4)QMake编译通过之后,继续将鼠标指针移动到左侧的工程名上并右击,在弹出的快捷菜单中选择“构建”命令。同样,此时下方的“编译输出”窗口没有出现异常错误提示,表示编译通过,此时才真正生成了可执行的.exe文件,如图1-43所示。

图1-43 编译输出

(5)编译生成.exe文件之后,可以单击Qt Creator窗口左下角的三角形按钮,运行编译通过的测试程序,程序运行结果如图1-44所示。注意其中的行数为2,而列数为2×3=6。

图1-44 例1-1程序运行结果

由此可知,在创建Mat对象时,对于二维多通道图像,首先要定义其尺寸,即行数和列数。然后需要指定存储元素的数据类型,以及每个矩阵点的通道数。为此,依据下面的规则有多种定义方法。

CV_[The number of bits per item][Signed or Unsigned][Type Prefix]C[The channel number]

例如前面例1-1中的“CV_8UC3”表示使用8个二进制位;U表示Unsigned int,即无符号整型;C代表所存储图像的通道;3代表所存储图像的通道数,每个像素由3个元素组成三通道。预先定义的通道数可以多达4个。Scalar是一个short整型的vector容器,指定这个参数能够使用指定的定制化值来初始化矩阵。

在C/C++中通过构造函数进行初始化,此处基于前面例1-1的方法进行了修改。

#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
//创建一个类型为8位uchar、颜色为三通道黄色的2×2 Mat对象
// Mat img(2,2,CV_8UC3,Scalar(0,255,255));
// cout <<"matrix element"<< endl << img << endl;
//创建一个超过二维的矩阵
 int sz[3] = { 2,2,2};

    //三维的Mat对象(2×2×2),元素全部为0
    Mat array2(3,sz,CV_8UC1,Scalar(0));
    //因为是三维的,所以不能用DOS命令行界面显示
    return 0;
}

上面演示了如何创建一个超过二维的矩阵:指定维数,然后传递一个指向一个数组的指针,这个数组包含每个维度的尺寸;其余的参数含义参考例1-1,此处为单通道图像,所以是CV_8UC1,Scalar(0)。

2.采用create()函数创建Mat对象

#include<opencv2/opencv.hpp>
#include<iostream>
using namespace cv;
using namespace std;
int main()
{
//创建一个类型为8位uchar、颜色为三通道黄色的2×2 Mat对象
// Mat img(2,2,CV_8UC3,Scalar(0,255,255));
// cout << "matrix element" << endl << img << endl;
//... 中间注释省略前面介绍的代码
//用create()函数实现对Mat对象的初始化
Mat img;
img.create(4,4,CV_8UC(2));
cout <<"M = " << endl << img << endl;
return 0;
}

程序运行结果如图1-45所示。

图1-45 create()函数创建的Mat对象

注意,这个创建Mat对象的方法不能为矩阵设初值,只是在改变尺寸时重新为矩阵数据开辟内存。

3.采用MATLAB样式初始化器cv::Mat::zeros、cv::Mat::ones、cv::Mat::eye创建Mat对象

用这种方法创建Mat对象时,需要指定要使用的矩阵大小和数据类型。

#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
//创建一个类型为8位uchar、颜色为三通道黄色的2×2Mat对象
// Mat img(2,2,CV_8UC3,Scalar(0,255,255));
// cout <<"matrix element"<< endl << img << endl;
//... 中间注释省略前面介绍的代码
Mat array1 = Mat::eye(4,4,CV_64F); // 对角矩阵
Mat array2 = Mat::ones(4,4,CV_32F); //全1矩阵
Mat array3 = Mat::zeros(4,4,CV_8UC1); //全0矩阵
cout <<"Diagonal matrix"<< endl << array1 << endl;
cout << "full one matrix" << endl << array2 << endl;
cout << "full zero matrix" << endl << array3 << endl;
return 0;
}

程序运行结果如图1-46所示。

图1-46 MATLAB样式初始化器创建的Mat对象

4.在小矩阵中可以用逗号分隔的初始化函数

//在小矩阵中可以用逗号分隔的初始化函数
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
//创建一个类型为8位uchar、颜色为三通道黄色的2×2 Mat对象
// Mat img(2,2,CV_8UC3,Scalar(0,255,255));
// cout << "matrix element" << endl << img << endl;
//... 中间注释省略前面介绍的代码
   Mat array = (Mat_<double>(3,3) << 0,-1,5,-1,5,-1,0,-1,0);
   cout << "matrix" << endl << array << endl;
   return 0;
}

程序运行结果如图1-47所示。

图1-47 在小矩阵中使用逗号分隔的初始化函数

5.使用clone()或者copyTo()函数为一个存在的Mat对象创建一个新的信息头

#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
//创建一个类型为8位uchar、颜色为三通道黄色的2×2 Mat对象
// Mat img(2,2, CV_8UC3,Scalar(0,255,255));
// cout << "matrix element" << endl << img << endl;
//... 中间注释省略前面介绍的代码
//使用clone()或者copyTo()函数为一个存在的Mat对象创建一个新的信息头
Mat srcImage(3,3,CV_8UC3,Scalar(0,0,255));
Mat copyImage;
srcImage.copyTo(copyImage);
Mat newImage = srcImage.row(1).clone();
cout << "matrix"<< endl << newImage << endl;
return 0;
}

程序运行结果如图1-48所示。

图1-48 使用clone()或者copyTo()函数为一个存在的Mat对象创建一个新的信息头

1.4 图像读取与保存

对于OpenCV中图像的读取,前文(1.2.4小节和1.2.5小节)已略有提及,本节将详细介绍OpenCV中图像的读取与保存方法。

· 1.4.1 图像读取

在OpenCV中,使用cv::imread()函数来读取和加载图像,该函数的形式如下。

Mat cv::imread(const String & filename, int flags = IMREAD_COLOR)          

可以看到,imread()函数的定义非常简单,其解析如下。

此外,imread()函数支持读取的常用图像格式有如下几种。

imread()函数中参数的标识flags值被定义在enum cv::ImreadModes枚举类里面,其值及含义如表1-1所示。

表1-1 imread()函数的参数flags的值及其含义

C++定义

说明

IMREAD_GRAYSCALE

如果设置,则始终将图像转换为单通道灰度图像(编/解码器内部转换)

IMREAD_COLOR

如果设置,则始终将图像转换为三通道BGR彩色图像(在OpenCV中,默认的颜色排列是BGR而非RGB)。imread()函数如不填写参数,默认使用这种方式读取图像

IMREAD_ANYCOLOR

如果设置,则以任何可能的颜色格式读取图像

IMREAD_REDUCED_GRAYSCALE_2

如果设置,则始终将图像转换为单通道灰度图像,图像尺寸减小1/2

IMREAD_REDUCED_COLOR_2

如果设置,则始终将图像转换为三通道BGR彩色图像,图像尺寸减小1/2

IMREAD_IGNORE_ORIENTATION

如果设置,则不会根据EXIF信息的方向标志旋转图像

下面将创建例1-2来实现并展示图像的读取操作。

例1-2:图像的读取。

具体操作过程如下。

(1)在.pro文件中配置OpenCV环境。

# 导入头文件
INCLUDEPATH+=D:/OpenCV/opencv/build/include
INCLUDEPATH+=D:/opencv/opencv/build/include/opencv2
# 导入库文件
win32:CONFIG(debug, debug|release): {
LIBS+=-LD:/OpenCV/opencv/build/x64/vc14/lib\
-lopencv_world440d
}
else{
LIBS+=-LD:/OpenCV/opencv/build/x64/vc14/lib\
-lopencv_world440
}

(2)编辑main.cpp文件(需要提前将touxiang.jpg文件放入D盘images文件夹中)。

#include<iostream>
#include<opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main()
{ //读取图像
    Mat image = imread("D:/images/touxiang.jpg");
    if (image.data != NULL)
    { //显示图像
        imshow("touxiang",image);
        waitKey(0);
    }
    else {
         cout << "can&apos;t open the file!" << endl;
         getchar();
    }
    return 0;
}

(3)对项目进行QMake编译和构建后,程序运行结果如图1-49所示。

图1-49 例1-2程序运行结果

注意

· 1.4.2 图像保存

同图像读取类似,OpenCV中使用cv::imwrite()函数实现将图像保存到指定的文件,该函数定义如下。

bool cv::imwrite(const String & filename,
InputArray img,
const std::vector<int> & params = std::vector<int>() );

imwrite()函数的参数解析如下。

需要注意的是,imwrite()函数是基于文件扩展名选择图像的格式。通常,使用此功能只能保存8位单通道或三通道(带有BGR通道顺序)图像,但有以下例外。

imwrite()函数中特定格式的编码参数及其含义如表1-2所示。

表1-2 imwrite()函数中特定格式的编码参数及其含义

编码参数

含义

IMWRITE_JPEG_QUALITY

JPEG格式图像的质量,其值可以是0~100(越大越好),默认值为95

IMWRITE_PNG_COMPRESSION

PNG格式图像的压缩级别,其值可以是0~9,值越大意味着越小的尺寸和越长的压缩时间。如果指定,则策略更改为IMWRITE_PNG_STRATEGY_DEFAULT(Z_DEFAULT_STRATEGY)。该参数默认值为1(最佳速度设置)

IMWRITE_PNG_BILEVEL

二进制级别PNG,其值为0或1,默认为0

IMWRITE_PXM_BINARY

PPM、PGM或PBM二进制格式标识,其值为0或1,默认值为1

IMWRITE_WEBP_QUALITY

覆盖EXR存储类型,默认为FLOAT(FP32)。代表WEBP格式图像的质量,其值可以是1~100(越大越好)。默认情况下(不带任何参数时),如果质量值大于100,则使用无损压缩

下面将通过例1-3展示图像的加载和保存操作。

例1-3:图像的加载和保存。

操作过程如下。

(1)在.pro文件中配置OpenCV环境。

# 导入头文件
INCLUDEPATH+=D:/OpenCV/opencv/build/include
INCLUDEPATH+=D:/opencv/opencv/build/include/opencv2
# 导入库文件
win32:CONFIG(debug, debug|release): {
LIBS+=-LD:/OpenCV/opencv/build/x64/vc14/lib\
-lopencv_world440d
}
else{
LIBS+=-LD:/OpenCV/opencv/build/x64/vc14/lib\
-lopencv_world440
}

(2)编辑main.cpp文件(需要提前将touxiang.jpg文件放入D盘images文件夹中)。

#include<opencv2/opencv.hpp>
#include<iostream>
using namespace cv;
using namespace std;
int main()
{
    Mat image;
    //加载图像
    image = cv::imread("D:/images/touxiang.jpg");
    if (image.empty())
    {
        cout << "can&apos;t open the file!" << endl;
        return -1;
    }
    imshow("main window", image);
    //保存图像到当前项目
    imwrite("D:/images/save.jpg", image);
    waitKey(0);
    //销毁所有窗口
    destroyAllWindows();
    return 0;
}

(3)程序运行结果如图1-50所示。

图1-50 例1-3程序运行结果

1.5 视频读取与输出

OpenCV中视频的读和写操作,分别是通过cv::VideoCapture和cv::VideoWriter两个类来实现的。

· 1.5.1 视频读取

cv::VideoCapture类是读取视频的,cv::VideoCapture既支持视频文件的读取,也支持从视频捕捉设备中读取视频。cv::VideoCapture类的对象创建方式有以下3种。

cv::VideoCapture capture(const string& filename,); 
// 第一种:从输入文件名对应的文件中读取
cv::VideoCapture capture(int device);
// 第二种:从视频捕捉设备 ID中读取视频
cv::VideoCapture capture();
//第三种:调用无参构造函数创建对象

第一种方式是从文件(AVI格式或者MP4格式等)中读取视频,对象创建完成以后,OpenCV将会打开文件并准备读取它。如果打开成功,将可以开始读取视频的帧,通过cv::VideoCapture类的成员函数isOpened()返回的true结果可以判断打开读取对象成功(建议在打开视频或摄像头时都使用该成员函数,以判断是否打开成功)。

第二种方式是从视频捕捉设备中读取视频。这种情况下,可以给出一个标识符,用于表示想要访问的视频捕捉设备,及其与操作系统的握手方式。对于视频捕捉设备而言,这个标识符就是一个标识数字——如果只有一台视频捕捉设备,那么就是0,如果系统中有多台视频捕捉设备,那么增加标识数字的值即可。

第三种方式仅创建一个捕获对象,而不提供任何关于打开的信息。对象创建以后通过成员函数open()来设定打开的信息。open()操作也有以下两种方式。

cv::VideoCapture cap;
cap.open( "my_video.avi"); //第一种方式打开视频文件
cap.open(0); //第二种方式打开视频捕捉设备

打开视频后,需要将视频帧读取到cv::Mat矩阵中,共有两种方式,一种是read()操作,另一种是>>操作。示例如下。

cv::Mat frame;
cap.read(frame); //读取方式一
cap >> frame; //读取方式二

综上,视频的读取操作步骤如下。

(1)创建cv::VideoCapture的对象。

cv::VideoCapture capture("D:/images/test1.mp4");

参数类型为const string&,即从文件中读取,若设置为0则读取视频捕捉设备。

(2)验证视频读取是否成功。

if (!capture.isOpened())
    {
        std::cout << "Vidoe open failed!" << std::endl;
        return -1;
    }

(3)验证完成,开始读取视频。

cv::Mat frame;
capture >> frame;

用户可以将VideoCapture对象像流一样读入Mat类型的对象(即图像)中。

下面将创建例1-4来展示视频文件的读取操作。

例1-4:视频文件读取。

项目完整代码文件(main.cpp文件)如下(需在1-4.pro文件中加入OpenCV配置信息,此处不再重复,详情可参考例1-3)。

#include<opencv2/opencv.hpp>
#include<iostream>
using namespace std;
using namespace cv;
int main()
{
      VideoCapture capture("D:/images/test1.mp4");
      if (!capture.isOpened())
      {
            cout << "Read video Failed !" << std::endl;
            return -1;
      }
      Mat frame;
      namedWindow("video test");
      int frame_num = capture.get(cv::CAP_PROP_FRAME_COUNT);
      cout << "total frame number is: " << frame_num << std::endl;
      for (int i = 0; i < frame_num - 1; ++i)
      {
                        capture >> frame;
                        //capture.read(frame); 第二种方式
                        imshow("video test", frame); //展示帧图像
                        if (cv::waitKey(30) == 'q')
                        {
                             break;
                        }
      }
      destroyWindow("video test"); //销毁窗口
      capture.release(); //释放对象
      return 0;
}

注意,上面的代码中使用cv::VideoCapture类的成员函数get()和设定标识cv::CAP_PROP_FRAME_COUNT获取了读取视频的总帧数。同样,可以通过指定其他标识来获取读取视频或视频捕捉设备的其他属性。

程序运行结果如图1-51所示。

图1-51 例1-4程序运行结果

· 1.5.2 视频输出

cv::VideoWriter类用于写入视频,该类使用起来比cv::VideoCapture类稍微复杂一些。cv::VideoWriter类的对象的创建有两种方式,第一种是使用构造函数,第二种是使用open()函数,具体示例如下。

第一种方式。

     cv::VideoWriter out(
     const string& filename, // 输出文件名
     int fourcc, // 编码形式,使用 CV_FOURCC()宏
     double fps, // 输出视频帧率
     cv::Size frame_size, // 单帧图像的大小
     bool is_color = true // 如果是false,则可传入灰度图像,true为彩色图像
);

第二种方式。

     cv::VideoWriter out;
     out.open(
     "my_video.mpg", //输出文件名
     CV_FOURCC('D','I','V','X'), // MPEG-4 编码
     30.0, // 输出视频帧率 
     cv::Size( 640, 480 ), // 单帧图像分辨率为 640像素×480像素
     true // 只可传入彩色图像,false为灰度图像
);

其中需要注意的是FOURCC编码形式,操作时常用以下函数把4个字符连接起来形成一个FOURCC 码,形式为cv::VideoWriter::fourcc(char c1,char c2,char c3,char c4)。

常用的格式有如下几种。

向创建的cv::VideoWriter对象写入图像也有两种方式,即write()操作和<<操作,示例如下。

     cv::VideoWriter::write(
                         const Mat& image // 写入图像作为下一帧
     );
     out << frame;

下面通过创建例1-5展示完整的视频读取(同例1-4,需要在1-5.pro文件中加入OpenCV配置信息)、处理及输出操作。

例1-5: 读取视频。

具体代码如下。

#include<stdio.h>
#include<opencv2/core/core.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>
#include<iostream>
using namespace std;
using namespace cv;
int main()
{
    Mat frame;
    string filename = "D:/images/test1.mp4";
    VideoCapture cap(filename);
    VideoWriter out;
    out.open("D:/images/out.mp4",
        cv::VideoWriter::fourcc('D','I','V','X'),
        cap.get(CAP_PROP_FPS),
        Size(cap.get(CAP_PROP_FRAME_WIDTH),cap.get(CAP_PROP_FRAME_HEIGHT)),
        true);
    if (!cap.isOpened())
    {
        cout << "Video load failed!" << endl;
        return -1;
    }
    while (1)
    {
        cap >> frame;
        if (frame.empty())
        {
            cout << "Video process finished!" << endl;
            return 0;
        }
        imshow("video",frame);
        if (waitKey(10) == 'q') break;
        out << frame;
    }
    cap.release();
    return 0;
}

注意,上面的代码中使用cv::VideoCapture类的成员函数get()和设定标识CAP_PROP_FPS获取读取视频的帧率。标识CAP_PROP_FRAME_WIDTH和CAP_PROP_FRAME_HEIGHT分别表示获取的视频图像的长度和宽度。

程序运行结果如图1-52所示。

图1-52 例1-5程序运行结果

1.6 图像属性与基本图形绘制

本节将介绍图像的属性定义和表示方法,以及如何绘制基本图形(如直线、矩形和圆形)。

· 1.6.1 图像属性

1. 画点 cv::Point()

OpenCV中使用cv::Point()表示其图像坐标和指定的2D点。cv::Point()类的对象创建有两种方式:第一种是使用构造函数,第二种是使用成员变量赋值。具体示例如下。

Point pt = Point(x,y) // 第一种方式:使用构造函数
Point pt; // 第二种方式:使用成员变量赋值
Pt.x = 10;
Pt.y = 8;

2. 定义颜色 cv::Scalar()

OpenCV中使用cv::Scalar()类来表示一个4元素的向量。其定义如下。

Scalar scalar(
double val[4];
)

Scalar类型被广泛应用于OpenCV中,常用于传递像素值,也常用于表示BGR颜色值(3个参数)。如果不使用最后一个参数,则无须定义最后一个参数。

当要定义一个颜色参数时,可以通过Scalar( a, b, c )来表示。例如,当要定义一个RGB颜色时,可以使Blue = a、Green = b、Red = c。

3. 设置尺寸 cv::Size()

OpenCV中图像的大小可以通过cv::Size类来表示,第一个参数width表示图像的宽度,第二个参数height表示图像的高度,其定义如下。

cv::Size(
    int _width,
    int _height
)

这里介绍的是Size类的简单使用方法,即可以通过“Size size(5, 10);”语句创建对象,通过size.width和size.height访问对象数据成员。

· 1.6.2 基本图形绘制

1.绘制直线

cv::line()函数用于在图像中绘制连接点pt1和点pt2的直线。cv::line()函数定义如下。

void cv::line(
     inputOutputArray img,//图像
     Point pt1,//点1
     Point pt2,//点2
     const Scalar & color,//绘制直线的颜色
     int thickness = 1,//直线的厚度
     int lineType = LINE_8,//直线的类型
     int shief = 0 //点坐标中的小数位数
)

2.绘制矩形

cv::rectangle()函数用于绘制矩形。OpenCV中通过cv::Rect类定义矩形,Rect矩形类包括Point点类的成员x和y(表示矩形的左上角)以及size类的成员width和height(表示矩形的大小)。但是,矩形类不会从Point点类或size类继承,因此通常不会从它们中继承操作符。其基本定义和使用方法如表1-3所示。

表1-3 cv::Rect类的定义与使用方法

功能

实现

默认构造函数

cv::Rect r;

复制构造函数

cv::Rect r2( r1 );

带值的构造函数

cv::Rect( x, y, w, h );

访问函数成员

r.x; r.y; r.width; r.height;

面积计算

r.area();

判断p点是否在r中

r.contains( p );

cv::rectangle()函数定义有以下两种形式。

形式一:

   void rectangle(
    cv::Mat& img,// 待绘制的图像
    cv::Point pt1,// 矩形的第一个顶点
    cv::Point pt2 // 矩形的对角顶点
    const cv::Scalar& color,// 线条的颜色(RGB)
    int lineType = 8,// 线型(4邻域或8邻域,默认为8邻域)
    int shift = 0 // 偏移量
   );

形式二:

   void rectangle(
    cv::Mat& img,// 待绘制的图像
    cv::Rect r,// 待绘制的矩形
    const cv::Scalar& color,// 线条的颜色(RGB)
    int lineType = 8,// 线型(4邻域或8邻域,默认为8邻域)
    int shift = 0 // 偏移量
   );

3.绘制圆形

cv::circle()函数用于绘制圆形。cv::circle()函数定义如下。

void circle(
cv::Mat& img,// 待绘制的图像
cv::Point center,// 圆心位置
int radius,// 圆的半径
const cv::Scalar& color,// 线条的颜色(RGB)
int thickness = 1,// 线宽
int lineType = 8,// 线型(4邻域或8邻域,默认为8邻域)
int shift = 0// 偏移量
);

下面将通过例1-6展示如何绘制点、直线、矩形和圆形等基本图形(同前面一样,需在1-6.pro文件中导入OpenCV环境配置信息)。

例1-6:基本图形绘制。

具体代码如下。

#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>
#include <iostream>
#include <stdio.h>
using namespace std;
using namespace cv;
int main()
{
// 设置窗口
Mat img = Mat::zeros(Size(800,600),CV_8UC3);
 
img.setTo(255);              // 设置屏幕为白色
Point p1(100,100);          // 点p1
Point p2(758,50);           // 点p2
// 画直线函数
line(img, p1, p2, Scalar(0,0,255), 2);   // 红色
line(img, Point(300,300), Point(758,400), Scalar(0,255,255), 3);
Point p(20,20);//初始化点p的坐标为(20,20)
circle(img,p,1,Scalar(0,255,0),-1);  // 画半径为1的圆形(画点)
Point p4;
p4.x = 300;
p4.y = 300;
circle(img, p4, 100, Scalar(120,120,120), -1);
int thickness = 3;
int lineType = 8;
double angle = 30;  //椭圆旋转角度
ellipse(img, Point(100,100), Size(90,60),angle,0,360, Scalar(255,255,0),thickness,lineType);
// 画矩形
Rect r(250,250,120,200);
rectangle(img,r,Scalar(0,255,255),3);
imshow(“pic”,img);
waitKey();
return 0;
}

程序运行结果如图1-53所示。

图1-53 例1-6程序运行结果

· 1.6.3 颜色空间转换

我们在生活中看到的彩色图像大多数是RGB类型的,但是在进行图像处理时,需要用到灰度、二值、HSV、HSI等颜色空间(又称颜色模式)。OpenCV中提供了cvtColor()函数来实现这些空间之间的相互转换。cvtColor()函数定义如下。

void cvtColor(InputArray src, OutputArray dst, int code[,int dstCn=0]);

该函数的参数解析如下。

该函数的作用是将图像从一个颜色空间转换到另一个颜色空间。应注意的是,从RGB颜色空间向其他颜色空间转换时,必须明确指出图像的颜色通道。前面提到过,在OpenCV中,默认的颜色排列是BGR而非RGB。所以对于24位颜色图像来说,前8位是蓝色,中间8位是绿色,最后8位是红色。常见的RGB通道的取值范围如下。

对于线性变换来说,这些取值范围是无关紧要的。但是对于非线性变换,输入的RGB图像必须归一化到其对应的取值范围来获得最终正确的转换结果,例如RGB→CIE L*u*v*转换。如果有一个32位浮点图像直接从8位图像转换而不进行任何缩放,那么它将具有0~255的值范围而不是该函数假定的0~1。所以,在调用cvtColor()函数之前,需要先将图像缩小。

img * = 1./255;
cvtColor(img,img,COLOR_BGR2Luv);

cvtColor()函数中的转换代码(code)如表1-4所示。

表1-4 转换代码(code)表

转换类型

转换代码(code)

RGBGRAY

COLOR_BGR2GRAY、COLOR_RGB2GRAY、COLOR_GRAY2BGR、COLOR_GRAY2RGB

RGBCIE XYZ.Rec

COLOR_BGR2XYZ、COLOR_RGB2XYZ、COLOR_XYZ2BGR、COLOR_XYZ2RGB

RGBYCrCb

COLOR_BGR2YCrCb、COLOR_RGB2YCrCb、COLOR_YCrCb2BGR、COLOR_YCrCb2RGB

RGBHSV

COLOR_BGR2HSV、COLOR_RGB2HSV、COLOR_HSV2BGR、COLOR_HSV2RGB

RGBHLS

COLOR_BGR2HLS、COLOR_RGB2HLS、COLOR_HLS2BGR、COLOR_HLS2RGB

RGBCIE L*a*b*

COLOR_BGR2Lab、COLOR_RGB2Lab、COLOR_Lab2BGR、COLOR_Lab2RGB

RGBCIE L*u*v*

COLOR_BGR2Luv、COLOR_RGB2Luv、COLOR_Luv2BGR、COLOR_Luv2RGB

BayerRGB

COLOR_BayerBG2BGR、COLOR_BayerGB2BGR、COLOR_BayerRG2BGR、COLOR_BayerGR2BGR、COLOR_BayerBG2RGB、COLOR_BayerGB2RGB、COLOR_BayerRG2RGB、COLOR_BayerGR2RGB

下面通过例1-7演示cvtColor()函数的使用方法。

例1-7: cvtColor()函数的使用方法。

具体代码如下。

#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main(int argc,char** argv)
{
     Mat src,dst,dst1;
     src = imread("D:/images/touxiang.jpg");
    //判断图像是否载入成功
    if (src.empty())
    {
        cout<< "can not open the image"<<endl;
        return -1;
    }
    imshow("touxiang.jpg",src);
    cvtColor(src,dst,COLOR_BGR2GRAY);//转换方式1,转换为灰度图,经常使用,需要记住
    cvtColor(src,dst1,COLOR_BGR2Lab);//转换方式2
    imshow("CV_BGR2GRAY change",dst);
    imshow("COLOR_BGR2Lab change",dst1);
    waitKey(0);
    return 0;
}

程序运行结果如图1-54所示。

图1-54 例1-7程序运行结果

1.7 计算机交互

在用OpenCV编制的程序运行过程中,可通过计算机鼠标、键盘等输入设备和屏幕等输出设备,以有效的方式实现人机交互。

· 1.7.1 鼠标和键盘

下面通过例1-8演示用鼠标绘制矩形的过程。

例1-8:用鼠标绘制矩形。

在这个例子中,鼠标左键被按下后,记录起始点(矩形),当鼠标左键被释放后,在起始点和释放点之间绘制一个矩形,如果按Esc键,则结束程序的运行。

程序代码和详细注释如下。

#include<opencv2\opencv.hpp>
#include<iostream>
#include<opencv2\highgui\highgui_c.h>
using namespace cv;
using namespace std;
Point pt;
void OnMouse(int event, int x, int y, int flags, void* param) {
//将param强制转换为Mat指针,*(Mat*)=Mat,就如*(int*)=int一样
    Mat img = *(Mat*)param; 
    switch (event) {
    case CV_EVENT_LBUTTONDOWN://鼠标左键被按下
        pt.x = x;
        pt.y = y;//记下起始点
        break;
    case CV_EVENT_LBUTTONUP://鼠标左键被释放
        rectangle(img, pt, Point(x, y), Scalar(0, 255, 0), 2, 8);//画矩形
        break;
    default:
        break;
    }
}
void main() {
    Mat img(500, 500, CV_8UC3, Scalar(255, 255,255));
    //创建图像并指定高、宽,格式、Scalar设置白色
    namedWindow("mouse", CV_WINDOW_AUTOSIZE);//创建窗口mouse
    setMouseCallback("mouse", OnMouse, &img);
    //在窗口mouse中加载鼠标事件,并传入img的地址
    while (1) {
        imshow("mouse", img);//在窗口mouse中显示绘制的图像
        char c = (char)waitKey(10);
        if (27 == c)//每隔10毫秒检测Esc键是否被按下
            break;//如果Esc被按下,则结束循环
        else
            printf("Other Operation\n");
    }
}

程序运行结果如图1-55所示。

图1-55 例1-8程序运行结果

· 1.7.2 滑动条

OpenCV中的滑动条是一种图形用户界面控件,通过鼠标拖动,可以选择需要的取值。下面通过例1-9演示如何通过滑动条来实现图像的缩放。

例1-9:利用滑动条实现图像缩放。

在这个例子中,通过滑动条选择不同的值,可以分别对图像进行20%、40%、60%、80%、100%比例的缩放。

#include<iostream>
#include<opencv2\opencv.hpp>
#include<opencv2\imgproc\types_c.h>
#include<opencv2\highgui\highgui_c.h>
using namespace std;
using namespace cv;
 int val=1;
 Mat srcImage,dstImage;
 void back(int,void*)
{
 Size dsize = Size(srcImage.cols*val/5, srcImage.rows*val/5);
 dstImage = Mat(dsize,CV_32S);
 resize(srcImage, dstImage, dsize);
 imshow("滑动条",dstImage);
}
int main()
{
namedWindow("源图像", WINDOW_NORMAL);//WINDOW_NORMAL表示窗口大小可以改变
srcImage = imread("lena.png");
imshow("源图像", srcImage);
namedWindow("滑动条", WINDOW_AUTOSIZE); //WINDOW_AUTOSIZE表示窗口自动适应大小
createTrackbar("比例20%,40%,60%,80%,100%", "滑动条", &val, 5, back);
//5个参数分别为滑动条的名字、窗口的名字(即滑动条对应窗口)、滑块值、最大值、回调函数
back(val,0);
waitKey();
}

程序运行结果如图1-56所示。

图1-56 例1-9程序运行结果

1.8 小结

本章主要介绍了OpenCV的概念和功能,读者可以根据需求搭建OpenCV编程环境。对于每一小节中的示例代码,建议读者去理解、复现,以加深对知识点的理解。本章后半部分还介绍了如何对图像和视频进行读取、展示和保存操作,以及基本图形的绘制和OpenCV与计算机的交互操作。本章的重点是OpenCV编程环境的搭建和对Mat容器的理解,以及图像、视频的读取和保存,读者需要加强练习,为学习后续章节打好基础。

相关图书

ChatGPT原理与应用开发
ChatGPT原理与应用开发
深度学习的数学——使用Python语言
深度学习的数学——使用Python语言
深度学习:从基础到实践(上、下册)
深度学习:从基础到实践(上、下册)
动手学深度学习(PyTorch版)
动手学深度学习(PyTorch版)
深度学习与医学图像处理
深度学习与医学图像处理
深度强化学习实战:用OpenAI Gym构建智能体
深度强化学习实战:用OpenAI Gym构建智能体

相关文章

相关课程