OpenCV-1-简介及开发环境搭建
1 前言
图形渲染OpenGL、计算机视觉OpenCV、音视频视频编解码FFmpeg是我之前一直想要做的三步曲学习计划,在完成图形渲染OpenGL的学习后,也顺利开始了新的篇章。计算机图形学现在的应用十分广泛,从实验室的机器人项目、医学领域、无人驾驶技术、VR和AR领域、特效美颜到游戏都能看到它的应用。特别是现今移动端的视频类应用更是随处可见,其中的诸如贴纸挂件、美颜滤镜等都离不开计算机视觉的应用。这些有趣炫酷的特效一直都吸引着我在这个领域不断前行,因为亲手做出这些炫酷的视觉应用能获得极大的乐趣。
在当前大多数的带特效的视频或者直播应用中,媒体数据流都离不开系统原生库或者FFmpeg采集原始视频资源,接着使用OpenCV检测关键点或者做物体追踪,再使用OpenGL处理前面两部得到的数据得到带特效的视频帧,最后使用系统库或者FFmpeg将这些媒体数据写入到文件中,或者包装成流数据用于直播业务。这也是为什么我计划媒体三步曲的原因之一。可能你也注意到了这三步曲都市跨平台的开源库,这无疑又让我更加着迷。
本系列是该三步曲的第二部,本文注意参考了由Adrian Kaehler和Gary Bradski所著的Learning OpenCV 3,并结合自己在学习时整理的一些经验完成。和写三步曲的第一部OpenGL,写本系列的目的有两个,一个原因是当作是自己的读书笔记便于日后查看,更重要的原因也是我所期待的是希望能认识更多在此路上前行的小伙伴。
2 概览
2.1 OpenCV概览
OpenCV是一个开源计算机视觉库,1999年Gary Bradski在英特尔公司发起了OpenCV项目,他的初衷是建立一个稳定的平台使得开发者能够在计算机视觉和人工智能领域更便捷的开始自己的项目,从而推动和加速该领域的发展。从OpenCV项目开始以来,它不断收到了来自英特尔和谷歌的支持,但只要来自于Itseez公司,该公司完成了早期的大部分工作,最近已经被英特尔公司收购。另外,Arraiy公司最近也加入到维护OpenCV的团队中。
该库使用C和C++语言开发,能够运行在Linux、Windows和Mac OS X平台上。此外目前该库正积极支持如Python、Java、MATLAB等更多语言,以及向iOS和Android移动平台迁移。
OpenCV聚焦于实时应用,它具有高效的计算效率。如果你想要在英特尔架构上得到进一步的自动优化,你可以购买英特尔发行的整合性能库(Integrated Performance Primitives,IPP),它由多个不同算法领域的底层优化组成。如果该库被成功安装,则OpenCV在运行时会自动使用合适的IPP库提升算法效率。在OpenCV3.0以后,英特尔授权给OpenCV团队和OpenCV社区一个免费版的IPP子集,通常被称为IPPICV,默认情况下OpenCV都会使用该库来提升算法效率。
OpenCV包含500多个函数,广泛设计视觉领域,包含工程生产监测、医学成像、安全、用户界面、相机标定、立体视觉、机器人等领域。计算机视觉和机器学习具有紧密的联系,因此OpenCV也包含一个完整的用于普通任务的机器学习库,即ML模组。该模组不仅在OpenCV的核心视觉任务重发挥重要的作用,它也足够应对大多数机器学习问题。
2.2 计算机视觉概览
计算机视觉指将照片和相机的数据转换为一个结论或者是一种新的表述。如可以得到结论“场景中有一个人”,或者“该切片中有14个肿瘤细胞”,或者将一副彩色图片转换为灰度图,或者在一个图片序列中移除相机移动的影响从而得到新的表述。
因为我们拥有视觉,我们可能会理所当然的任务计算机视觉的任务很简单,如场景中存在一辆汽车。我们的直觉有时可能会误导我们,实际上这是一个极其复杂的任务。大脑会将视觉信号分为多个通道,并将其处理为不同的信息。我们的大脑还有注意力系统,它能够基于当前我们要完成的任务识别出图片中重要的区域,从而过滤其他无关的细节。另外在视觉信息流中还存在大量我们知之甚少的反馈效应来完成当前任务。此外得益于触感等其他感官信息我们的大脑能够利用多用的生活经验来联想,从而辅助视觉任务。反馈循环会作用于信息处理的各个阶段,包含硬件传感器本身,如控制虹膜调整光线强度,如调整视网膜来改变感知的细节。
但是在机器视觉系统中,计算机接收到的数据仅仅是来自于相机或者磁盘的一个数字网格。最重要的是,计算机本身不具备模式识别能力,也不能自动控制聚焦和光圈,没有联想能力,它还是一个非常原始的系统。下图是一个汽车的照片,我们能够轻易的认出图片中的汽车后视镜,然而对于计算机而言它看到的仅仅是一个数字矩阵。并且在这些数据中还存在噪声,这使得计算机能够感知到的有效信息进一步减少。我们的任务就是让计算机识别出这一组数据是汽车后视镜,可以想到计算机视觉是一个多么艰难的任务。
实际上,计算机视觉的问题远不止如此复杂。对于一个3D世界场景投影到2D平面的图像,在重建其3D场景时没有一个唯一的解。也就是说多个不同的3D场景可以投影出一个相同的2D图像,或者如下图同一个3D场景在不同的角度可以得到不同的2D投影。
另外,采样得到的数据也会被噪声或者图像扭曲污染。这些数据污染源于真实世界中的天气条件、光照条件、折射现象或者移动等,透镜或者机械装置的瑕疵或者图片捕捉后得数据压缩都可能污染数据。
在实践系统中,通常使用额外的上下文信息来弥补计算机系统相较于我们的视觉传感器的劣势。想象一下机器人在建筑物内寻找一个订书机的场景,机器人可以参考如下事实来完成任务,即书桌通常位于办公室中,而订书机通常位于书桌上。其中包含了隐含的物体尺寸线索,即订书机能够放在桌子上,另外该线索也可以过滤掉目标不可能出现的场景,如天花板或者窗户。因此机器人完全可以忽略200英尺的广告飞艇,尽管它们的形状看上去像一个订书机。另外对于图像检索类任务,摄影者在拍摄照片通常会将物体放在场景中央,并且以能够体现其特征的角度拍摄照片,这里面通常也包含一些无意的隐含信息。
上下文信息也可以通过机器学习技术直接建模获取,隐藏的诸如尺寸和重力方向等信息可以通过关联标记好的训练集关联得到。另外一种方法是使用额外的传感器获取这些隐含的变量,如使用激光测距仪获得的景深能够帮助我们更精确的测量物体的尺寸。
正如前文所讲到,计算机视觉面临的另外一个问题是噪声,通常我们使用统计学的方法来处理该问题。例如,仅仅只比较一个点和相邻像素点很难判断这是否是一个边缘,但是观察一定区域内的统计学结果,如果直接相邻像素点色值相差现象能够连成一条线,那么这通常就是一个边缘。另外我们也可以从时间维度上的统计数据来削弱噪声影响。此外还有一些技术通过使用已有的数据建立显式的模型来处理噪声和图像扭曲问题。如棱镜造成的图像扭曲机制已经很明朗,通过描述图像扭曲的多项式模型参数就能分析扭曲图像。
计算机视觉作出的行为或者决议基于相机数据,并且受指定的目标或者任务的上下文限制。在办公室游荡的机器人的视觉软件和静止的安全相机的视觉软件会采取不同的策略,因为这两个系统的上下文以及它们的目标是不同的。通常的规则是,计算机视觉的上下文限制越高,我们就更能充分的使用这些限制来简化问题,最终的解决方案也会越可靠。
在某些场景下,OpenCV中的高层函数足以处理计算机视觉中的复杂问题,即使不能,那么我们仍能通过该库中的基础组件来构建一个完整的方案来处理几乎所有的计算机视觉问题。在后面的例子中会提供一些经过验证可靠的使用OpenCV的方法,刚开始这些方法都会使用尽可能多的可用组建来解决问题,在掌握这个初版解决方案后,你可以发现这些方案中的缺陷,并且使用自己的才智和代码来修复这些缺陷。示例中的场景和你实际遇到的场景通常存在差异,建议使用自己的代码解决实际遇到的问题,而不是例子给出的固定场景问题,
2.3 OpenCV的起源
OpenCV诞生于英特尔的一个面向CPU密集型应用的研究项目,英特尔在该项目结束后发起了很多工程,其中包括实时光线追踪和3D显示墙等。其中的一个作者Gary Bradski当时在英特尔工作,当时他拜访了很多大学注意到了一些顶级的大学中的研究组织,如MIT媒体实验室有发达的内部开源的计算机视觉工程,这些代码在学生中传递,这使得新的学生能够有一个有价值的基础设施从而完成它们自己的视觉应用。
正是在这样的环境中,OpenCV被设计于提供一个更广泛的计算机视觉基础架构。在英特尔性能库团队(Intel’s Performance Library Team)的帮助下,OpenCV完成了核心部分的代码,并且将算法规范送到英特尔俄罗斯团队的成员中以获得更多专业优化意见。整个OpenCV演进的过程如下图所示。
OpenCV最初的目标是:
- 提供开源的并经过优化的基本视觉架构代码从前促进高级视觉项目研究
- 通过提供通用的开发者能够使用的基础架构传播视觉知识,因此代码设计考虑了易读和易移植性
- 通过提供可移植的、高性能的、免费的代码促进更多的基于高级视觉的商业应用,OpenCV的许可文件不要求使用到该库的应用开源或者必须免费提供
2.4 OpenCV的分层
OpenCV具有多层结构,最上层是其运行的操作系统,其下是特定的语言绑定层和采样应用,其下是opencv_contrib,它包含了大多数高层函数,再之下是OpenCV的核心功能层,最底层是硬件加速层(Hardware acceleration layer, HAL),该层提供了大量的硬件优化。
2.5 IPP加速
如果在英特尔的处理器环境中运行,OpenCV可以利用英特尔集成性能库(Integrated Performance Primitives, IPP)的一个免费子集,IPP8.x(IPPICV)来提供程序性能。在使用Cmake编译OpenCV时,可以打开IPPICV设置(WITH_IPP=ON/OFF,默认情况下时ON)将其连接到OpenCV中,从而替代相应的底层优化的C语言代码。IPP可以带来很大的性能收益,具体如下图。
2.6 下载和安装OpenCV
OpenCV使用Git做版本控制,它的代码托管在GitHub网站上,你也可以从OpenCV的官方网站获取最新版本的源码。另外前文说到的OpenCV高层级函数opencv_contrib,它的代码作为一个单独的仓储同样使用Git管理,并托管在GitHub网站上。OpenCV是一个跨平台的开源库,需要使用CMake编译得到二进制库。尽管其官网提供了一些编译好的库,或者你也能从网上获取到特定平台的库,但是通常为了定制我们实际的应用场景,我们需要自己编译。
需要注意的是OpenCV和opencv_contrib是两个独立的仓储,前者由核心OpenCV团队维护,其中包含成熟并且稳定的功能,后者由OpenCV社区维护,其中可能包含受专利保护的算法,因此这部分功能在商用时可能还需要做一些额外的工作。
2.6.1 macOS
macOS平台上可以通过如下方式编译安装OpenCV的二进制库,随后在终端中使用相关函数,或者将其添加到XCode工程中,完成自己的应用程序。这里仅介绍如何得到二进制库,具体使用实例后文介绍。
- 确定已经安装最新的CMake,如果未安装,可以使用HomeBrew安装,或者在官网下载CMake GUI。使用CMake命令行修改变量并生成配置环境,也可以使用CMake GUI选择Unix makefile,点击generate生成适合当前环境的默认配置,再做必要的个性化修改即可,如去掉不想编译模块的勾选状态。再使用Make编译即可得到一系列二进制库。官方教程
- 使用HomeBrew安装最新稳定版本的OpenCV。官网安装指令
2.6.2 iOS
iOS平台中可以通过如下方式编译得到OpenCV的二进制库,也可以将这些二进制的打包成苹果推出的Framework文件,然后将其添加到XCode工程中,以完成自己的应用程序。这里仅介绍如何得到二进制库和Framework文件,具体使用实例后文介绍。
- 通过CMake生成配置环境,再通过Make编译得到二进制库。
- 通过源码仓库中附带的脚本生成二进制库和Framework文件,需要注意的是完整版本的OpenCV库较大,这里需要使用脚本提供的指令精简库所支持的架构,以及需要编译的模块从而精简库的大小。官网安装指令
- 使用CocoaPods集成OpenCV。官网集成指令
- 直接从官网或者GitHub获取Framework包。GitHub下载地址
2.6.3 其他平台
其他平台库的集成方式可以在OpenCV官方网站上找到相应的教程,或者也可以从本文的参书目Learning OpenCV 3中获取更多细节。
2.7 OpenCV文档
从3.0版本开始,OpenCV不再提供任何离线文档,而是以在线文档的方式提供,另外我们还可以参考OpenCV在GitHub上的网站Wiki页面,这些文档以及它们相关的文档可以分为如下几个部分。
- 函数介绍,这部分包含各个模块中函数的介绍和参数的说明。
- 集成教程,这部分包含如何在不同平台上编译和安装OpenCV。
- 使用教程,这部分按照不同的模块来介绍OpenCV所有功能。
- 速查表,这歌PDF文档是OpenCV的极精简查询手册。
- Wiki页面,这里可以找到如何成为一个OpenCV贡献者的方法。
- 问答社区,你可以在这里发现大量关于OpenCV的问题,从中发现自己遇到问题的解决方案,你也可以在这里提出和回答问题。
另外网上还有资料介绍了整个OpenCV以及OpenCV3,关于加速的问题可以在这里找到。
3 OpenCV初探
3.1 头文件
OpenCV按模块的方式组织其头文件,整个OpenCV提供一个头文件opencv.hpp
,其中包含了对每个模块头文件的引用。对于每个模块都提供一个相应的主要头文件,并按模块名配置一个文件夹,其中包含该模块的所有头文件,如core模块的主要头文件core.hpp
。通常对头文件保持最小依赖,即仅依赖方法和类型声明的头文件,以加快编译速度。
3.2 展示一张图片
OpenCV中ImageCodecs模块可以读取多种格式的图片文件,HighGUI模块可以建立窗口并展示图片数据,我们第一个例子利用这两个模块加载并显示一直图片。这里使用macOS环境的XCode工具来建立工程,首先创建一个macOS平台的命令行工程,然后将安装好的OpenCV动态库libopencv_core.dylib
、libopencv_highgui.dylib
、libopencv _imgcodecs.dylib
、libopencv_imgproc.dylib
添加到工程中,然后在main.cpp文件中引入上述两个模块头文件。
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>
在开始项目之前,先编译整工程,此时可能会遇到以下几个错误。
- 如果报动态库无法加载,首先确保已经安装OpenCV,然后请在工程选项-当前Target选项-General选项-Framework and libraries中检查所依赖的动态库,在文件资源的Frameworks中删除相应的动态库,再添加自己安装好的动态库。
- 如果报动态库无法加载,并提示无动态库签名,请在终端使用命令
codesign -f -s "Mac Developer: <你的苹果开发者账号邮箱>" *.dylib
对某个动态库签名,由于动态库存在依赖问题,你也可以使用命令codesign -f -s "Mac Developer: <你的苹果开发者账号邮箱>"/usr/local/opt/*/lib/*.dylib
对该目录下的所有动态库签名。 - 如果在编译时OpenCV报头文件无法找到,请确保在工程选项-当前Target选项-Build Settings选项中搜索Header Search Paths,将其改为你自己在/usr/local下安装好的包含OpenCV2头文件集合的文件夹,需要注意是要设置为OpenCV2的上级目录,否则编译时会报错。
加载并显示一个图片的主要函数如下,为了保正XCode项目能够正常运行,需要在Edit Schem中的Argument选项中设置主函数的额外参数,主函数默认有1个参数,因此这里是从第二个参数开始设置。另外这里的文件路径需要使用绝对路径。当一切就绪后,运行该项目就能看懂图片被正确的显示出来。你可以可以在命令行中运行编译得到的可执行文件,并设置正确的文件路径作为额外参数也可以显示图片。示例DisplayAPicture源码传送门
int main(int argc, const char * argv[]) {
cv::Mat img = cv::imread(argv[1], cv::IMREAD_UNCHANGED);
if (img.empty()) {
return -1;
}
cv::namedWindow("Example1", cv::WINDOW_AUTOSIZE);
cv::imshow("Example1", img);
cv::waitKey(0);
cv::destroyWindow("example1");
return 0;
}
函数cv::imread()
从磁盘中读取图片文件到内存中,它根据文件名来决定读取图片的格式,该函数支持多种图片格式,包括BMP、DIB、JPEG、JPE、PNG、PBM、PGM、PPM、SR、RAS和TIFF。 类cv::Mat
可以表示所有类型的图片,包括单通道、多通道、整型、浮点型格式的图片。
函数v::namedWindow()
创建并打开一个窗口,第一个参数是该窗口对象的索引,模块HighGUI中其他和窗口相关的函数都通过该索引来获取唯一的对象,第二个参数设置为0
使用默认大小,此时窗口大小固定,图片伸缩,通常设置为WINDOW_AUTOSIZE
即使用图片的大小改变窗口尺寸。
函数cv::imshow()
可以展示一个图片对象,如果当前没有窗口对象,会隐式的创建一个窗口。该函数调用后,窗口会重绘易显示该图片,如果创建窗口时选择了WINDOW_AUTOS IZE
策略,则窗口大小调整为图片大小。
函数cv::waitKey()
可以挂起程序,等待用户的任意键盘输入,参数为正时表示程序挂起的毫秒数,参数为0或者负时表示程序一直挂起。cv::Mat
对象受自动内存管理,其内部包含引用计数机制,也就是说和STL中的对象一样,当该对象出作用域后会被自动释放,我们不需要担心内存泄漏的问题。
函数cv::destroyWindow()
会关闭掉窗口并释放相关的内存,在窗口对象出作用域之前一定要确保该函数被调用,以避免内存泄漏问题。
3.3 播放视频
播放视频文件的例子主要代码如下。示例PlayVideo源码传送门
int main(int argc, const char * argv[]) {
cv::namedWindow("Example-PlayVideo");
cv::VideoCapture cap;
cap.open(argv[1]);
std::cout << "Opened file: " << argv[1] << std::endl;
cv::Mat frame;
for (;;) {
cap >> frame;
if (frame.empty()) {
break;
}
cv::imshow("Example-PlayVideo", frame);
if ((char)cv::waitKey(33) >= 0) {
break;
}
}
return 0;
}
上述代码中类cv::VideoCapture
的实例可以管理视频资源,通过其实例函数open()
可以打开ffmpeg支持格式的视频文件。文件成功打开后,该实例会包含所有的视频信息,包括状态信息。使用这种方式打开视频文件后,该实例指向了视频文件的头部。
通过类cv::Mat
的实例可以持有视频流里的每帧画面,在循环内部通过代码cap >> frame
将cap
指向的当前帧数据逐帧读到frame
中,代码frame.empty()
判断如果当前帧读取错误则退出循环,通过函数cv::imshow()
将图片显示到当前窗口中,通过函数cv::waitKey()
控制帧显示时长为33毫秒,并且监听是否有用户键盘输入事件。
3.3.1 添加进度条
接下来可以为上面的程序添加一个进度条,使得用户可以控制视频播放的进度。程序设置了视频预览的两种模式,仅加载显示下一帧的单步模式,也是默认模式,可以通过敲击S键切换到该模式,和持续播放的播放模式,可以通过敲击R键切换至该模式。
首先通过全局变量g_run
来控制播放器的状态,当其取值为正时表示等待播放的剩余帧数(其中当前为1时表示单步模式),当前为0时表示暂停状态,当前为-1为播放模式,即连续播放剩余帧。另外还定义了全局的cv::VideoCapture
实例,用于管理视频对象。
int g_run = 1;
cv::VideoCapture g_cap;
接着声明并实现如下进度条回掉函数,其函数类型需要参考HighGUI模块提供的函数cv::createTrackbar()
的头文件。下面的代码中,当通过鼠标操作进度条后会将播放器切换到单步模式。在该函数中使用到了cv::VideoCapture
实例方法set()
来配置进度条位置,类似的还有该实例的get()
方法可以获得更多视频对象信息。
void onTrackbarSlide(int pos, void *) {
g_cap.set(cv::CAP_PROP_POS_FRAMES, pos);
if (g_run != 1) {
g_run = 1;
}
}
在主函数中,首先通过cv::VideoCapture
实例方法get()
获取视频的帧数,再调用函数cv::createTrackbar()创建一个进度条,并将游标位置设置为起始值,这里参数的含义依次是进度条的标识,显示的窗口标识,游标的初始位置,游标的总长度,鼠标点击游标的回调函数。
int frames = (int)g_cap.get(cv::CAP_PROP_FRAME_COUNT);
int g_slider_position = 0;
cv::createTrackbar("Position",
"Example-AddTrackbar",
&g_slider_position,
frames,
onTrackbarSlide);
接下来只需要创建一个循环根据当前的状态来读取帧数据并将其显示到窗口中即可,循环内部核心代码如下。这里只需要注意不要忘记调用函数cv::setTrackbarPos()
更新进度条的游标位置。
if (g_run != 0) {
g_cap >> frame;
if (frame.empty()) {
break;
}
int current_pos = (int)g_cap.get(cv::CAP_PROP_POS_FRAMES);
cv::setTrackbarPos("Position", "Example-AddTrackbar", current_pos);
cv::imshow("Example-AddTrackbar", frame);
g_run -= 1;
}
最后再监听键盘输入,并切换播放器状态即可。示例AddTrackbar源码传送门
3.4 图像滤镜
示例ImageFilter使用ImageProc模块的相关函数对图片应用高斯模糊,其核心代码如下。核心函数cv::GaussianBlur()
前两个参数分别表示原始图片数据以及生成的图片对象。第三个参数是二维卷积核的大小,需要注意的是它在两个维度上的取值都必须是基数,因为我们需要对其中心像素应用模糊算法。接下来两个参数分别是高斯模糊算法数学公式即正态分布在x和y方向上的标准差,数值越大,概率分布越宽,中心像素权重越低,模糊程度越高。示例ImageFilter源码传送门
cv::Mat image = cv::imread(argv[1], cv::IMREAD_UNCHANGED);
cv::Mat out;
cv::GaussianBlur(image, out, cv::Size(5,5), 3, 3);
3.5 图像下采样
示例PyramidImage使用函数cv::pyrDown()
下采样某张图片至其原始尺寸的一半大小,该函数内部包含了高斯模糊算法。多次对一个图像使用这种方式下采样,我们将得到一个图像序列,被称为尺度空间(Scale Space)或者图片金字塔(Image Pyramid),这种图像序列在计算机视觉中常用于处理被观察到场景尺寸发生变化的情况。
如果你熟悉呢奎斯特采样理论,很容易理解信号的下采样等同于利用一系列脉冲函数(Delta Functions)对信号执行卷积操作,在当前场景下信号就是图像。这种采样方式会为得到的结果引入高频噪声,因此需要通过低通滤波器处理原始信号使其低于采样频率。示例PyramidImage的核心代码如下。示例PyramidImage源码传送门
// 显示原始图片
cv::namedWindow(c_window_name_in, cv::WINDOW_AUTOSIZE);
cv::Mat imageIn = cv::imread(argv[1]);
cv::imshow(c_window_name_in, imageIn);
// 显示使用一半尺寸下采样处理后的图片
cv::namedWindow(c_window_name_out, cv::WINDOW_NORMAL);
cv::Mat imageOut;
cv::pyrDown(imageIn, imageOut);
cv::imshow(c_window_name_out, imageOut);
3.6 边缘检测
示例EdgeDetector可以检测一张图片的边缘,首先需要使用函数cv::cvtColor()将原始图片转换为灰度图,再调用函数cv::Canny()检测该灰度图的边缘并得到处理后的图片,该示例核心代码如下。这里暂时不详细展开边缘检测的算法,具体算法会在后面的章节中详细介绍。示例EdgeDetector源码传送门
cv::Mat image_rgb = cv::imread(argv[1]);
cv::Mat image_gray;
cv::cvtColor(image_rgb, image_gray, cv::COLOR_RGB2GRAY);
cv::Mat image_edge;
cv::Canny(image_gray, image_edge, 10, 100, 3, true);
示例ShowPixels在示例EdgeDetector基础上进行修改,它通过下采样原始图像的灰度图至1/4尺寸得到一张新的图片,再对该图应用了边缘检测效果。并通过函数_Tp& Mat::at(int i0, int i1)
获取并修改了特定位置的像素数据,其核心代码如下。示例ShowPixels源码传送门
cv::Vec3b intensity = image_rgb.at<cv::Vec3b>(y, x);
uchar blue = intensity[0];
uchar green = intensity[1];
uchar red = intensity[2];
image_edge.at<uchar>(x, y) = 128;
3.7 从相机获取数据
从相机获取图像数据的方式和打开视频文件类似,示例Camera调用方法CV::VideoCapture open()
,其参数index是硬件资源的索引,当只有1个相机硬件时,如本示例中传入0即可。示例Camera源码传送门
// 从文件读取视频数据
CV_WRAP virtual bool open(const String& filename, int apiPreference = CAP_ANY);
// 从相机读取视频数据
CV_WRAP virtual bool open(int index, int apiPreference = CAP_ANY);
3.8 生成视频文件
示例VideoExporter从文件中读取视频数据,逐帧处理图像后将其写入到文件中。类cv::VideoWriter提供视频数据写入功能,函数cv::VideoWriter open()打开一个视频输入会话,其参数分别指定了数据写入的目标文件路径,使用的编码器代号,视频写入的帧率,每帧画面的大小。需要注意的是OpenCV并不包含编解码器,如果使用了不被支持的编码格式,会使得视频导出失败。函数cv::logPolar()使用对数极坐标的方式处理图片,随后代码writer << logpolar_frame将处理好的帧放入导出器中,最终被写入到文件中。本实例涉及的函数和算法在后面章节会详细展开,其核心代码如下。示例VideoExporter源码传送门
// 创建视频捕捉对象,并采集视频文件相关信息
cv::VideoCapture capture(argv[1]);
cv::Size size (
(int)capture.get(cv::CAP_PROP_FRAME_WIDTH),
(int)capture.get(cv::CAP_PROP_FRAME_HEIGHT)
);
double fps = capture.get(cv::CAP_PROP_FPS);
// 创建视频导出器对象,并开启视频写入会话
cv::VideoWriter writer;
int fcc = fourCharCode('M', 'J', 'P', 'G');
writer.open(argv[2], fcc, fps, size);
// 通过循环的方式处理视频流,并将帧数据放入导出器中
cv::Mat original_frame, logpolar_frame;
for (; ; ) {
// 读取帧数据
capture >> original_frame;
// 得到对数极坐标转换后的图像
cv::logPolar(
original_frame,
logpolar_frame,
cv::Point2f(
original_frame.cols/2,
original_frame.rows/2
),
40,
cv::WARP_FILL_OUTLIERS
);
// 将处理后的帧数据放入导出器
writer << logpolar_frame;
}
总结
读到到这里,应该有如下效果。
- 了解OpenCV是什么,了解它能帮我吗完成什么任务。认识到计算机视觉是一个非常复杂的任务。
- 了解OpenCV的分层结构,了解OpenCV贡献库和OpenCV的关系。
- 能够编译自己工作平台的OpenCV库,并能够使用函数简单的展示一幅图片。