原文:annas-archive.org/md5/b2288be5348a598b7d3a1975121fa894

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

开源计算机视觉项目,如 OpenCV 3,使各种用户能够利用机器视觉、机器学习和人工智能的力量。通过掌握这些强大的代码和知识库,专业人士和爱好者可以在任何需要的地方创建更智能、更好的应用程序。

这正是本书关注的焦点,通过一系列动手项目和模板引导你,教你如何结合出色的技术来解决你特定的难题。

在我们研究计算机视觉时,让我们从这些话中汲取灵感:

*“我看见智慧胜过愚昧,正如光明胜过黑暗。”
传道书,2:13

让我们构建能够清晰“看”的应用程序,并创造知识。

本书涵盖的内容

第一章, 充分利用您的相机系统,讨论了如何选择和配置相机系统以看到不可见的光、快速运动和远距离物体。

第二章, 使用自动相机拍摄自然和野生动物,展示了如何构建自然摄影师使用的“相机陷阱”,并处理照片以创建美丽的效果。

第三章, 使用机器学习识别面部表情,探讨了使用各种特征提取技术和机器学习方法构建面部表情识别系统的途径。

第四章, 使用 Android Studio 和 NDK 进行全景图像拼接应用,专注于构建 Android 全景相机应用程序的项目,借助 OpenCV 3 的拼接模块。我们将使用 C++和 Android NDK。

第五章, 工业应用中的通用目标检测,研究了优化你的目标检测模型,使其具有旋转不变性,并应用特定场景的约束,使其更快、更稳健。

第六章, 使用生物特征属性进行高效的人脸识别,是关于基于该人的生物特征(如指纹、虹膜和面部)构建人脸识别和注册系统。

第七章,陀螺仪视频稳定,展示了融合视频和陀螺仪数据的技术,如何稳定使用手机拍摄的短视频,以及如何创建超高速视频。

您需要为此书准备的材料

作为基本设置,整本书基于 OpenCV 3 软件。如果章节没有特定的操作系统要求,那么它将在 Windows、Linux 和 Mac 上运行。作为作者,我们鼓励您从官方 GitHub 仓库的最新 master 分支(github.com/Itseez/opencv/)获取 OpenCV 安装,而不是使用官方 OpenCV 网站(opencv.org/downloads.html)上的可下载包,因为最新 master 分支包含与最新稳定版本相比的大量修复。

对于硬件,作者们期望您有一个基本的计算机系统设置,无论是台式机还是笔记本电脑,至少有 4GB 的 RAM 内存可用。其他硬件要求如下。

以下章节在 OpenCV 3 安装的基础上有特定的要求:

第一章,充分利用您的相机系统

  • 软件:OpenNI2 和 FlyCapture 2。

  • 硬件:PS3 Eye 相机或任何其他 USB 网络摄像头,华硕 Xtion PRO live 或任何其他 OpenNI 兼容的深度相机,以及一个或多个镜头的 Point Grey Research (PGR)相机。

  • 备注:PGR 相机设置(使用 FlyCapture 2)在 Mac 上无法运行。即使你没有所有必需的硬件,你仍然可以从中受益于本章的一些部分。

第二章,使用自动相机拍摄自然和野生动物

  • 软件:Linux 或 Mac 操作系统。

  • 硬件:带有电池的便携式笔记本电脑或单板计算机(SBC),结合一台照相机。

第四章,使用 Android Studio 和 NDK 进行全景图像拼接应用

  • 软件:Android 4.4 或更高版本,Android NDK。

  • 硬件:任何支持 Android 4.4 或更高版本的移动设备。

第七章,陀螺仪视频稳定

  • 软件:NumPy、SciPy、Python 和 Android 5.0 或更高版本,以及 Android NDK。

  • 硬件:一部支持 Android 5.0 或更高版本的智能手机,用于捕获视频和陀螺仪信号。

基本安装指南

作为作者,我们承认在您的系统上安装 OpenCV 3 有时可能相当繁琐。因此,我们添加了一系列基于您系统上最新的 OpenCV 3 master 分支的基本安装指南,用于安装 OpenCV 3 以及为不同章节工作所需的必要模块。有关更多信息,请参阅github.com/OpenCVBlueprints/OpenCVBlueprints/tree/master/installation_tutorials

请记住,本书还使用了来自 OpenCV “contrib”(贡献)存储库的模块。安装手册将提供如何安装这些模块的说明。然而,我们鼓励您只安装我们需要的模块,因为我们知道它们是稳定的。对于其他模块,情况可能并非如此。

本书面向对象

如果您渴望构建比竞争对手更智能、更快、更复杂、更实用的计算机视觉系统,这本书非常适合您。这是一本高级书籍,旨在为那些已经有一定 OpenCV 开发环境和使用 OpenCV 构建应用程序经验的读者编写。您应该熟悉计算机视觉概念、面向对象编程、图形编程、IDE 和命令行。

习惯用法

在本书中,您将找到许多不同的文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“您可以通过访问opencv.org并点击下载链接来找到 OpenCV 软件。”

代码块如下所示:

Mat input = imread("/data/image.png", LOAD_IMAGE_GRAYSCALE);
GaussianBlur(input, input, Size(7,7), 0, 0);
imshow("image", input);
waitKey(0);

当我们希望将您的注意力引向代码块中的特定部分时,相关的行或项目将以粗体显示:

Mat input = imread("/data/image.png", LOAD_IMAGE_GRAYSCALE);
GaussianBlur(input, input, Size(7,7), 0, 0);
imshow("image", input);
waitKey(0);

任何命令行输入或输出如下所示:

新术语重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:“点击下一步按钮将您带到下一屏幕。”

注意

警告或重要注意事项将以如下所示的框中出现。

小贴士

小技巧和窍门如下所示。

读者反馈

我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢或不喜欢的地方。读者反馈对我们来说很重要,因为它帮助我们开发出您真正能从中获得最大价值的标题。

要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及本书的标题。

如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在您是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从www.packtpub.com下载示例代码文件,这是您购买的所有 Packt 出版物的账户。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

代码也由本书的作者在 GitHub 仓库中维护。此代码仓库可在github.com/OpenCVBlueprints/OpenCVBlueprints找到。

下载本书的颜色图像

我们还为您提供了一个包含本书中使用的截图/图表的颜色图像的 PDF 文件。这些颜色图像将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/B04028_ColorImages.pdf下载此文件。

勘误

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。

要查看之前提交的勘误表,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分。

由于本书也有一个分配给它的 GitHub 仓库,您也可以通过在以下页面创建问题来报告内容勘误:github.com/OpenCVBlueprints/OpenCVBlueprints/issues

盗版

互联网上版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,请立即提供位置地址或网站名称,以便我们可以追究补救措施。

请通过mailto:copyright@packtpub.com与我们联系,并提供疑似盗版材料的链接。

我们感谢您在保护我们作者以及为我们提供有价值内容的能力方面提供的帮助。

问题

如果您在这本书的任何方面遇到问题,您可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决问题。或者,如之前所述,您可以在 GitHub 仓库中提出一个问题,作者之一将尽快帮助您。

第一章. 充分利用您的相机系统

克劳德·莫奈,法国印象派绘画的创始人之一,教导他的学生只画他们看到的,而不是他们知道的。他甚至走得更远,说:

“我希望我生来就是盲人,然后突然恢复视力,这样我就可以在不知道面前看到的是什么物体的情况下开始绘画。”

莫奈拒绝传统的艺术主题,这些主题往往具有神秘、英雄、军事或革命性质。相反,他依靠自己对中产阶级生活的观察:社会远足;阳光明媚的花园、池塘、河流和海滨;雾蒙蒙的林荫大道和火车站;以及私人损失。他带着深深的悲伤告诉他的朋友,乔治·克雷孟梭(未来的法国总统):

“有一天,我发现自己在看着我最爱的妻子的死人脸,然后系统地根据自动反射来记录颜色!”

莫奈根据他个人的印象画了一切。晚年,他甚至画出了自己视力衰退的症状。当他患有白内障时,他采用了红色调色板,而在白内障手术后,由于他的眼睛对光更敏感,可能接近紫外范围,他采用了明亮的蓝色调色板。

就像莫奈的学生一样,我们作为计算机视觉学者必须面对看到知道之间的区别,以及输入和处理之间的区别。光、镜头、相机和数字成像管道可以赋予计算机一种视觉感。从产生的图像数据中,机器学习(ML)算法可以提取知识或者至少是一套元感知,如检测、识别和重建(扫描)。如果没有适当的感觉或数据,系统的学习潜力将受到限制,甚至可能是零。因此,在设计任何计算机视觉系统时,我们必须考虑照明条件、镜头、相机和成像管道的基础要求。

为了清晰地看到某个特定对象,我们需要什么?这是我们第一章的核心问题。在这个过程中,我们将解决五个子问题:

  • 我们需要什么才能看到快速运动或光的变化?

  • 我们需要什么才能看到远处的物体?

  • 我们需要什么才能拥有深度感知?

  • 我们在黑暗中需要看到什么?

  • 在购买镜头和相机时,我们如何获得物有所值?

小贴士

对于许多计算机视觉的实际应用,环境并不是一个光线充足、白色的房间,而且主题也不是距离 0.6 米(2 英尺)的人脸!

硬件的选择对这些问题至关重要。不同的相机和镜头针对不同的成像场景进行了优化。然而,软件也可以决定解决方案的成功或失败。在软件方面,我们将关注 OpenCV 的高效使用。幸运的是,OpenCV 的videoio模块支持许多类型的相机系统,包括以下几种:

  • 在 Windows,Mac 和 Linux 中通过以下框架使用网络摄像头,这些框架是大多数操作系统版本的标准部分:

    • Windows: Microsoft Media Foundation (MSMF), DirectShow 或 Video for Windows (VfW)

    • Mac: QuickTime

    • Linux: Video4Linux (V4L), Video4Linux2 (V4L2), 或 libv4l

  • iOS 和 Android 设备中的内置摄像头

  • 通过 OpenNI 或 OpenNI2 兼容的深度相机,OpenNI 和 OpenNI2 是在 Apache 许可证下开源的

  • 通过专有的 Intel Perceptual Computing SDK 使用其他深度相机

  • 通过 libgphoto2 使用照相机,libgphoto2 是在 GPL 许可证下开源的。有关 libgphoto2 支持的照相机列表,请参阅gphoto.org/proj/libgphoto2/support.php

    注意

    注意,GPL 许可证不适用于闭源软件的使用。

  • 通过 libdc1394 兼容的 IIDC/DCAM 工业相机,libdc1394 是在 LGPLv2 许可证下开源的

  • 对于 Linux,unicap 可以用作 IIDC/DCAM 兼容相机的替代接口,但 unicap 是 GPL 许可证,因此不适用于闭源软件。

  • 其他工业相机通过以下专有框架:

    • Allied Vision Technologies (AVT) PvAPI 用于 GigE Vision 相机

    • Smartek Vision Giganetix SDK 用于 GigE Vision 相机

    • XIMEA API

注意

OpenCV 3 中的 videoio 模块是新的。在 OpenCV 2 中,视频捕获和录制是 highgui 模块的一部分,但在 OpenCV 3 中,highgui 模块仅负责 GUI 功能。有关 OpenCV 模块的完整索引,请参阅官方文档docs.opencv.org/3.0.0/

然而,我们不仅限于 videoio 模块的功能;我们可以使用其他 API 来配置相机和捕获图像。如果一个 API 可以捕获图像数据数组,OpenCV 可以轻松地使用这些数据,通常无需任何复制操作或转换。例如,我们将通过 OpenNI2(无需 videoio 模块)捕获和使用深度相机的图像,以及通过 Point Grey Research (PGR)的 FlyCapture SDK 捕获工业相机的图像。

注意

工业相机或机器视觉相机通常具有可互换的镜头,高速硬件接口(例如 FireWire,千兆以太网,USB 3.0 或 Camera Link),以及所有相机设置的完整编程接口。

大多数工业相机都为 Windows 和 Linux 提供 SDK。PGR 的 FlyCapture SDK 支持 Windows 上的单相机和多相机设置,以及 Linux 上的单相机设置。PGR 的一些竞争对手,如 Allied Vision Technologies (AVT),在 Linux 上提供更好的多相机设置支持。

我们将学习不同类别相机之间的差异,并将测试几种特定镜头、相机和配置的能力。到本章结束时,你将更有资格为自己、实验室、公司或客户设计消费级或工业级视觉系统。我希望通过每个价格点的可能结果来让你感到惊讶!

调色光

人类眼睛对某些电磁辐射的波长很敏感。我们把这些波长称为“可见光”、“颜色”,有时也只称为“光”。然而,我们对“可见光”的定义是以人为中心的,因为不同的动物看到不同的波长。例如,蜜蜂对红光视而不见,但可以看到紫外线(这对人类来说是不可见的)。此外,机器可以根据几乎任何刺激(如光、辐射、声音或磁性)组装出人类可见的图像。为了拓宽我们的视野,让我们考虑八种电磁辐射及其常见来源。以下是按波长递减顺序的列表:

  • 无线电波 来自某些天体和闪电。它们也由无线电子设备(无线电、Wi-Fi、蓝牙等)产生。

  • 微波 从大爆炸辐射出来,作为宇宙中的背景辐射存在。微波炉也能产生微波。

  • 远红外线 (FIR) 光 是来自温暖或热物体(如热血动物和热水管)的无形光芒。

  • 近红外线 (NIR) 光 从我们的太阳、火焰和红热或几乎红热的金属中明亮地辐射出来。然而,它在常见的电照明中是一个相对较弱的组成部分。叶子和其他植被会明亮地反射 NIR 光。皮肤和某些织物对 NIR 稍微透明。

  • 可见光 从我们的太阳和常见的电光源中明亮地辐射出来。可见光包括红、橙、黄、绿、蓝和紫(按波长递减的顺序)。

  • 紫外线 (UV) 光 在阳光下也很丰富。在晴朗的日子里,紫外线可以烧伤我们的皮肤,并且可以以远处的蓝灰色雾霾的形式对我们稍微可见。常见的硅酸盐玻璃对紫外线几乎是透明的,所以我们站在窗户后面(室内或车内)时不会晒伤。同样地,紫外线相机系统依赖于由非硅酸盐材料(如石英)制成的镜头。许多花朵上有紫外线标记,这些标记对昆虫来说是可见的。某些体液(如血液和尿液)对紫外线的透明度比对可见光的透明度更高。

  • X 射线 来自某些天体,如黑洞。在地球上,氡气和某些其他放射性元素是自然的 X 射线来源。

  • 伽马射线 来自核爆炸,包括超新星爆炸。伽马射线的一些来源还包括放射性衰变和闪电。

美国国家航空航天局提供了以下与每种光线或辐射相关的波长和温度的视觉表示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00002.jpeg

被动成像系统依赖于前面所述的环境(常见)光线或辐射源。主动成像系统包括自己的光源,这样光线或辐射可以以更可预测的方式结构化。例如,一个主动夜视仪可能使用一个近红外线相机和一个近红外线光源。

对于天文学,被动成像在整个电磁谱上都是可行的;浩瀚的宇宙充满了来自新旧来源的各种光线和辐射。然而,对于地球(地面)用途,被动成像主要限于远红外线、近红外线、可见光和紫外线范围。主动成像在整个谱上都是可行的,但功耗、安全性和干扰(在我们的用例与其他用例之间)的实用性限制了我们可以将过多的光线和辐射倾注到环境中的程度。

不论是主动还是被动,成像系统通常使用镜头将光线或辐射聚焦到相机传感器的表面。镜头及其涂层会传输某些波长的光线,同时阻挡其他波长的光线。可以在镜头或传感器前面放置额外的滤光片以阻挡更多波长的光线。最后,传感器本身表现出变化的光谱响应,这意味着对于某些波长的光线,传感器会记录一个强烈的(明亮)信号,而对于其他波长的光线,则记录一个微弱的(暗淡)信号或没有信号。通常,大规模生产的数字传感器对绿色的响应最强,其次是红色、蓝色和近红外线。根据使用情况,这样的传感器可能会配备一个滤光片来阻挡一定范围内的光线(无论是近红外线还是可见光)以及/或一个滤光片来叠加不同颜色的图案。后者允许捕获多通道图像,如 RGB 图像,而未过滤的传感器将捕获单色(灰色)图像。

传感器的表面由许多敏感点或像素组成。这些与捕获的数字图像中的像素类似。然而,像素和像素不一定是一对一对应的。根据相机系统的设计和配置,几个像素的信号可能会混合在一起,以创建一个多通道像素的邻域、一个更亮的像素或一个更少噪声的像素。

考虑以下成对的图像。它们展示了一个带有拜耳滤光片的传感器,这是一种常见的颜色滤光片,每个红色或蓝色像素旁边有两个绿色像素。为了计算单个 RGB 像素,需要混合多个像素值。左侧图像是显微镜下过滤传感器的照片,而右侧图像是展示滤光片和下方像素的剖视图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00003.jpeg

注意

前面的图像来自维基媒体。它们是由用户 Natural Philo 贡献的,遵循 Creative Commons Attribution-Share Alike 3.0 Unported 许可协议(左侧),以及 Cburnett 贡献的,遵循 GNU 自由文档许可协议(右侧)。

如此例所示,一个简单的模型(RGB 像素)可能会隐藏关于数据捕获和存储方式的重要细节。为了构建高效的图像处理管道,我们需要考虑的不仅仅是像素,还包括通道和宏像素——共享某些数据通道的像素邻域,它们作为一个块被捕获、存储和处理。让我们考虑三种图像格式类别:

  • 原始图像是光电传感器的光电信号的实际表示,缩放到某些范围,如 8、12 或 16 位。对于传感器给定行中的光电传感器,数据是连续的,但对于给定列中的光电传感器,数据不是连续的。

  • 打包图像将每个像素或宏像素连续存储在内存中。也就是说,数据是按照它们的邻域进行排序的。如果我们的大部分处理都涉及多个颜色组件,这是一个高效的格式。对于典型的彩色相机,原始图像不是打包的,因为每个邻域的数据分布在多行中。打包彩色图像通常使用 RGB 通道,但也可以使用YUV 通道,其中 Y 是亮度(灰度),U 是蓝色(与绿色相对),V 是红色(也与绿色相对)。

  • 平面图像将每个通道连续存储在内存中。也就是说,数据是按照它们所代表的颜色组件进行排序的。如果我们的大部分处理都涉及单个颜色组件,这是一个高效的格式。打包彩色图像通常使用 YUV 通道。在平面格式中有一个 Y 通道对于计算机视觉来说效率很高,因为许多算法都是设计用来处理灰度数据的。

一张来自单色相机的图像可以有效地以原始格式存储和处理,或者(如果它必须无缝集成到彩色成像管道中)作为平面 YUV 格式中的 Y 平面。在本章的后续部分,在Supercharging the PlayStation EyeSupercharging the GS3-U3-23S6M-C and other Point Grey Research cameras等节中,我们将讨论示例代码,展示如何高效处理各种图像格式。

到目前为止,我们已经简要介绍了光、辐射和颜色的分类——它们的来源、与光学和传感器的相互作用,以及它们作为通道和邻域的表示。现在,让我们探索图像捕获的一些更多维度:时间和空间。

在瞬间捕捉主题

罗伯特·卡帕,一位报道过五场战争并在奥马哈海滩拍摄了 D-Day 登陆第一波次的摄影记者,给出了以下建议:

“如果你的照片不够好,那是因为你离得不够近。”

就像计算机视觉程序一样,摄影师是镜头背后的智慧。(有些人会说摄影师是镜头背后的灵魂。)一位优秀的摄影师会持续执行检测和跟踪任务——扫描环境,选择主题,预测将创造正确拍照时刻的动作和表情,并选择最有效地框定主题的镜头、设置和视角。

通过“足够接近”主题和动作,摄影师可以用肉眼快速观察细节,并且因为距离短且设备通常较轻(与三脚架上的长焦镜头相比),可以快速移动到其他视角。此外,近距离、广角拍摄将摄影师和观众拉入事件的第一人称视角,就像我们成为主题或主题的同伴,在那一刻一样。

照片美学在第二章中进一步探讨,即使用自动相机拍摄自然和野生动物。现在,我们只需确立两条基本规则:不要错过主体,不要错过时机!能见度差和时机不佳是摄影师或计算机视觉实践者能给出的最糟糕的借口。为了自我监督,让我们定义一些与这些基本规则相关的测量标准。

分辨率是镜头和相机能看到的细节的最精细程度。对于许多计算机视觉应用来说,可识别的细节是工作的主题,如果系统的分辨率差,我们可能会完全错过这个主题。分辨率通常用传感器的像素计数或捕获图像的像素计数来表示,但最好的这些测量只能告诉我们一个限制因素。一个更好的、经验性的测量,它反映了镜头、传感器和设置的各个方面,被称为每毫米线对数(lp/mm)。这意味着在给定设置中,镜头和相机可以分辨的最大黑白线条密度。在高于这个密度的任何情况下,捕获图像中的线条会模糊在一起。请注意,lp/mm 会随着主题的距离和镜头的设置(包括变焦镜头的焦距(光学变焦))而变化。当你接近主题,放大或用长焦镜头替换短焦镜头时,系统当然应该捕捉到更多的细节!然而,当你裁剪(数字变焦)捕获的图像时,lp/mm 不会变化。

照明条件和相机的ISO 速度设置也会影响 lp/mm。高 ISO 速度在低光下使用,它们增强了信号(在低光中较弱)和噪声(始终很强)。因此,在高 ISO 速度下,一些细节被增强的噪声所掩盖。

要接近其潜在分辨率,镜头必须正确对焦。当代摄影师丹特·斯特拉描述了现代相机技术的一个问题:

“首先,它缺乏……思维控制的预测自动对焦。”

这意味着,当其算法与特定、智能的使用或场景中条件变化的特定模式不匹配时,自动对焦可能会完全失败。长焦镜头在不当对焦方面尤其不容忍。景深(焦点最近点和最远点之间的距离)在长焦镜头中较浅。对于某些计算机视觉设置——例如,悬挂在装配线上的相机——主题的距离是高度可预测的,在这种情况下,手动对焦是一个可接受的解决方案。

**视野(FOV)**是镜头视野的范围。通常,视野是以角度来测量的,但也可以测量为从镜头特定深度处两个可观察到的边缘点的距离。例如,90 度的视野也可以表示为在 1 米深度处的 2 米视野或在 2 米深度处的 4 米视野。除非另有说明,否则视野通常指的是对角线视野(镜头视野的对角线),而不是水平视野或垂直视野。长焦镜头的视野较窄。通常,长焦镜头也有更高的分辨率和更少的畸变。如果我们的主题超出了视野范围,我们就会完全错过主题!在视野的边缘,分辨率往往会降低,畸变往往会增加,因此,视野最好足够宽,以便在主题周围留出一定的空间。

相机的吞吐量是它捕获图像数据的速率。对于许多计算机视觉应用,视觉事件可能在一瞬间开始和结束,如果吞吐量低,我们可能会完全错过这一刻,或者我们的图像可能会受到运动模糊的影响。通常,吞吐量以每秒帧数(FPS)来衡量,但将其作为比特率来衡量也可能很有用。吞吐量受以下因素的影响:

  • 快门速度(曝光时间):对于曝光良好的图像,快门速度受光照条件、镜头的光圈设置和相机的 ISO 速度设置限制。(相反,较慢的快门速度允许更窄的光圈设置或更慢的 ISO 速度。)在讨论完这个列表后,我们将讨论光圈设置。

  • 快门类型:全局快门会在所有光电传感器上同步捕获。滚动快门则不会;相反,捕获是顺序进行的,传感器底部的光电传感器比顶部的光电传感器晚记录信号。滚动快门较差,因为它会在物体或相机快速移动时使物体看起来倾斜。(这有时被称为“果冻效应”,因为视频与摇摆的果冻山相似。)此外,在快速闪烁的照明下,滚动快门会在图像中创建明暗条纹。如果捕获的开始是同步的,但结束不是,则该快门被称为全局重置的滚动快门

  • 相机内置图像处理程序,例如将原始光电传感器信号转换为给定格式中给定数量的像素。随着像素数和每像素字节数的增加,吞吐量会降低。

  • 相机与主机计算机之间的接口:按位速率递减的顺序,常见的相机接口包括 CoaXPress 全、Camera Link 全、USB 3.0、CoaXPress 基础、Camera Link 基础、千兆以太网、IEEE 1394b(FireWire 全)、USB 2.0 和 IEEE 1394(FireWire 基础)。

宽光圈设置可以让更多的光线进入,以便允许更快的曝光、更低的 ISO 速度或更明亮的图像。然而,窄光圈具有提供更大景深的优点。镜头支持有限的光圈设置范围。根据镜头的不同,某些光圈设置比其他设置具有更高的分辨率。长焦镜头往往在光圈设置上表现出更稳定的分辨率。

镜头的光圈大小以f 值f 挡表示,这是镜头焦距与其光圈直径的比率。粗略地说,焦距与镜头的长度有关。更精确地说,当镜头聚焦于无限远的目标时,它是相机传感器与镜头系统光学中心之间的距离。焦距不应与焦距混淆——即对焦物体的距离。以下图表说明了焦距和焦距以及视场(FOV)的含义:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00004.jpeg

使用更高的 f 值(即比例更窄的光圈),镜头会传输更少的光。具体来说,传输光的强度与 f 值的平方成反比。例如,当比较两个镜头的最大光圈时,摄影师可能会写,“f/2 镜头比 f/2.8 镜头快一倍。”这意味着前一个镜头可以传输两倍的光强度,允许在半数时间内进行等效曝光。

镜头的效率透射率(透射光的比例)不仅取决于光圈数,还取决于非理想因素。例如,一些光线被镜头元件反射而不是透射。T 数T 挡是根据特定镜头的透射率经验发现对光圈数进行调整。例如,无论其光圈数如何,T/2.4 镜头的透射率与理想 f/2.4 镜头相同。对于电影镜头,制造商通常提供 T 数规格,但对于其他镜头,更常见的是只提供光圈数规格。

传感器的效率是指镜头传输的光线中到达光电元件并转换为信号的比例。如果效率低下,传感器会错过大部分光线!效率更高的传感器倾向于在更广泛的相机设置、镜头设置和光照条件下拍摄曝光良好的图像。因此,效率为系统提供了更多自由度来自动选择对分辨率和吞吐量最优的设置。对于前一小节中描述的常见传感器类型,彩色光线,颜色滤光片的选取对效率有很大影响。设计用于以灰度形式捕捉可见光的相机具有高效率,因为它可以在每个光电元件上接收所有可见波长。设计用于在多个颜色通道中捕捉可见光的相机通常效率较低,因为每个光电元件会过滤掉一些波长。仅设计用于捕捉近红外光(NIR)的相机,通过过滤掉所有可见光,通常具有更低的效率。

效率是系统在多种光照(或辐射)条件下形成某种图像能力的好指标。然而,根据主题和实际光照,一个相对低效的系统可能具有更高的对比度和更好的分辨率。选择性过滤波长的优势并不一定反映在 lp/mm 上,这是衡量黑白分辨率的指标。

到现在为止,我们已经看到了许多可量化的权衡,这些权衡使得我们捕捉瞬间的主题变得复杂。正如罗伯特·卡帕的建议所暗示的,使用短焦距镜头靠近主题是一个相对稳健的方案。它允许以最小的风险获得良好的分辨率,并且不太可能完全错过构图或焦点。另一方面,这种设置存在高畸变和定义上的短工作距离。超越卡帕时代的相机功能,我们还考虑了允许高吞吐量和高效视频捕获的功能和配置。

在了解了波长、图像格式、相机、镜头、捕获设置和摄影师的常识之后,我们现在可以挑选几个系统来研究。

收集不同寻常的嫌疑人

本章的演示应用程序使用三台摄像头进行测试,这些摄像头在以下表格中描述。演示程序也与许多其他摄像头兼容;我们将在每个演示的详细描述中讨论兼容性。这三台选定的摄像头在价格和功能方面差异很大,但每台都能做普通网络摄像头做不到的事情!

名称 价格 用途 模式 光学
索尼 PlayStation Eye $10 在可见光中进行被动、彩色成像 640x480 @ 60 FPS320x240 @ 187 FPS 视场角:75 度或 56 度(两种缩放设置)
华硕 Xtion PRO Live $230 在可见光中进行被动、彩色成像,在近红外光中进行主动、单色成像,深度估计 彩色或近红外:1280x1024 @ 60 FPS 深度:640x480 @ 30 FPS 视场角:70 度
PGR Grasshopper 3 GS3-U3-23S6M-C $1000 在可见光中进行被动、单色成像 1920x1200 @ 162 FPS C-mount 镜头(不包括)

注意

关于我们可以在 GS3-U3-23S6M-C 摄像头中使用的镜头示例,请参阅本章后面的“选购玻璃”部分。

我们将尝试将这些摄像头的性能推到极限。使用多个库,我们将编写应用程序来访问不寻常的捕获模式,并快速处理帧,以便输入成为瓶颈。借用 1950 年代肌肉车设计师的术语,我们可以说我们想要“超级充电”我们的系统;我们希望向它们提供专业或过剩的输入,看看它们能做什么!

Supercharging the PlayStation Eye

索尼于 2007 年开发了 Eye 摄像头,作为 PlayStation 3 游戏的输入设备。最初,没有其他系统支持 Eye。从那时起,第三方为 Linux、Windows 和 Mac 开发了驱动程序和 SDK。以下列表描述了这些第三方项目的一些当前状态:

  • 对于 Linux 系统,gspca_ov534 驱动程序支持 PlayStation Eye,并且与 OpenCV 的 videoio 模块配合使用时无需额外设置。此驱动程序是大多数最新 Linux 发行版的标准配置。当前版本的驱动程序支持高达 320x240 @ 125 FPS 和 640x480 @ 60 FPS 的模式。即将发布的版本将增加对 320x240 @ 187 FPS 的支持。如果您想今天升级到这个未来版本,您需要熟悉 Linux 内核开发的基础知识,并自行构建驱动程序。

    注意

    github.com/torvalds/linux/blob/master/drivers/media/usb/gspca/ov534.c 查看驱动程序的最新源代码。简要来说,您需要获取您 Linux 发行版内核的源代码,合并新的 ov534.c 文件,将驱动程序作为内核的一部分构建,最后加载新构建的 gspca_ov534 驱动程序。

  • 对于 Mac 和 Windows,开发人员可以使用名为 PS3EYEDriver 的 SDK 将 PlayStation Eye 支持添加到他们的应用程序中,该 SDK 可在github.com/inspirit/PS3EYEDriver找到。尽管名称如此,此项目不是一个驱动程序;它在应用程序级别支持摄像头,但在操作系统级别不支持。支持的模式包括 320x240 @ 187 FPS 和 640x480 @ 60 FPS。该项目附带示例应用程序代码。PS3EYEDriver 中的大部分代码源自 GPL 许可的 gspca_ov534 驱动程序,因此,PS3EYEDriver 的使用可能仅适用于也是 GPL 许可的项目。

  • 对于 Windows,可以从 Code Laboratories (CL) 购买商业驱动程序和 SDK,网址为codelaboratories.com/products/eye/driver/。在撰写本文时,CL-Eye Driver 的价格为 3 美元。然而,该驱动程序与 OpenCV 3 的 videoio 模块不兼容。依赖于驱动程序的 CL-Eye Platform SDK,额外费用为 5 美元。支持的最快模式是 320x240 @ 187 FPS 和 640x480 @ 75 FPS。

  • 对于较新的 Mac 版本,没有可用的驱动程序。一个名为 macam 的驱动程序可在webcam-osx.sourceforge.net/找到,但它最后一次更新是在 2009 年,并且不适用于 Mac OS X Mountain Lion 和更新的版本。

因此,Linux 中的 OpenCV 可以直接从 Eye 摄像头捕获数据,但 Windows 或 Mac 中的 OpenCV 需要另一个 SDK 作为中介。

首先,对于 Linux,让我们考虑一个使用 OpenCV 根据 Eye 的高速输入录制慢动作视频的 C++应用程序的最小示例。此外,程序应记录其帧率。让我们称这个应用程序为“不眨眼”Eye。

注意

“不眨眼”Eye 的源代码和构建文件位于本书 GitHub 仓库的github.com/OpenCVBlueprints/OpenCVBlueprints/tree/master/chapter_1/UnblinkingEye

注意,此示例代码也应适用于其他与 OpenCV 兼容的摄像头,尽管与 Eye 相比,帧率较慢。

“不眨眼”Eye 可以实现在一个名为UnblinkingEye.cpp的单个文件中,包含这些几行代码:

#include <stdio.h>
#include <time.h>

#include <opencv2/core.hpp>
#include <opencv2/videoio.hpp>

int main(int argc, char *argv[]) {

  const int cameraIndex = 0;
  const bool isColor = true;
  const int w = 320;
  const int h = 240;
  const double captureFPS = 187.0;
  const double writerFPS = 60.0;
  // With MJPG encoding, OpenCV requires the AVI extension.
  const char filename[] = "SlowMo.avi";
  const int fourcc = cv::VideoWriter::fourcc('M','J','P','G');
  const unsigned int numFrames = 3750;

  cv::Mat mat;

  // Initialize and configure the video capture.
  cv::VideoCapture capture(cameraIndex);
  if (!isColor) {
    capture.set(cv::CAP_PROP_MODE, cv::CAP_MODE_GRAY);
  }
  capture.set(cv::CAP_PROP_FRAME_WIDTH, w);
  capture.set(cv::CAP_PROP_FRAME_HEIGHT, h);
  capture.set(cv::CAP_PROP_FPS, captureFPS);

  // Initialize the video writer.
  cv::VideoWriter writer(
      filename, fourcc, writerFPS, cv::Size(w, h), isColor);

  // Get the start time.
  clock_t startTicks = clock();

  // Capture frames and write them to the video file.
  for (unsigned int i = 0; i < numFrames;) {
    if (capture.read(mat)) {
      writer.write(mat);
      i++;
    }
  }

  // Get the end time.
  clock_t endTicks = clock();

  // Calculate and print the actual frame rate.
  double actualFPS = numFrames * CLOCKS_PER_SEC /
      (double)(endTicks - startTicks);
  printf("FPS: %.1f\n", actualFPS);
}

注意,摄像头指定的模式是 320x240 @ 187 FPS。如果我们的 gspca_ov534 驱动程序版本不支持此模式,我们预计它将回退到 320x240 @ 125 FPS。同时,视频文件指定的模式是 320x240 @ 60 FPS,这意味着视频将以慢于实际速度的速度播放,作为特殊效果。可以使用以下终端命令构建“不眨眼”Eye:

$ g++ UnblinkingEye.cpp -o UnblinkingEye -lopencv_core -lopencv_videoio

构建“不眨眼”Eye,运行它,记录一个移动的物体,观察帧率,并播放录制的视频SlowMo.avi。你的物体在慢动作中看起来如何?

在 CPU 或存储速度较慢的机器上,Unblinking Eye 可能会因为视频编码或文件输出的瓶颈而丢弃一些捕获的帧。不要被低分辨率所迷惑!在 320x240 @ 187 FPS 模式下,摄像头的数据传输速率大于在 1280x720 @ 15 FPS 模式下(一个稍微卡顿的 HD 分辨率)。通过将像素乘以帧率,可以看到每种模式下每秒传输了多少像素。

假设我们想要通过捕获和记录单色视频来减少每帧的数据量。当 OpenCV 3 在 Linux 上构建时,如果启用了 libv4l 支持,则此选项可用。(相关的 CMake 定义是WITH_LIBV4L,默认开启。)通过更改 Unblinking Eye 中的以下代码行并重新构建,我们可以切换到灰度捕获:

const bool isColor = false;

注意,对这个布尔值的更改会影响以下代码中突出显示的部分:

  cv::VideoCapture capture(cameraIndex);
  if (!isColor) {
 capture.set(cv::CAP_PROP_MODE, cv::CAP_MODE_GRAY);
 }
  capture.set(cv::CAP_PROP_FRAME_WIDTH, w);
  capture.set(cv::CAP_PROP_FRAME_HEIGHT, h);
  capture.set(cv::CAP_PROP_FPS, captureFPS);

  cv::VideoWriter writer(
      filename, fourcc, writerFPS, cv::Size(w, h), isColor);

在幕后,VideoCaptureVideoWriter对象现在使用平面 YUV 格式。捕获的 Y 数据被复制到一个单通道的 OpenCV Mat中,并最终存储在视频文件的 Y 通道中。同时,视频文件的 U 和 V 颜色通道只是填充了中间值,128,用于灰度。U 和 V 的分辨率低于 Y,因此在捕获时,YUV 格式只有每像素 12 位(bpp),而 OpenCV 的默认 BGR 格式为 24 bpp。

注意

OpenCV 视频 io 模块中的 libv4l 接口目前支持以下cv::CAP_PROP_MODE的值:

  • cv::CAP_MODE_BGR(默认)以 BGR 格式捕获 24 bpp 颜色(每个通道 8 bpp)。

  • cv::CAP_MODE_RGB以 RGB 格式捕获 24 bpp 颜色(每个通道 8 bpp)。

  • cv::CAP_MODE_GRAY从 12 bpp 平面 YUV 格式中提取 8 bpp 灰度。

  • cv::CAP_MODE_YUYV以打包的 YUV 格式(Y 为 8 bpp,U 和 V 各为 4 bpp)捕获 16 bpp 颜色。

对于 Windows 或 Mac,我们应该使用 PS3EYEDriver、CL-Eye Platform SDK 或其他库来捕获数据,然后创建一个引用数据的 OpenCV Mat。以下部分代码示例展示了这种方法:

int width = 320, height = 240;
int matType = CV_8UC3; // 8 bpp per channel, 3 channels
void *pData;

// Use the camera SDK to capture image data.
someCaptureFunction(&pData);

// Create the matrix. No data are copied; the pointer is copied.
cv::Mat mat(height, width, matType, pData);

事实上,几乎任何数据源集成到 OpenCV 的方法都是相同的。相反,为了将 OpenCV 作为其他库的数据源使用,我们可以获取存储在矩阵中的数据的指针:

void *pData = mat.data;

在本章的后面部分,在Supercharging the GS3-U3-23S6M-C and other Point Grey Research cameras中,我们介绍了一个将 OpenCV 与其他库集成的微妙示例,特别是 FlyCapture2 用于捕获和 SDL2 用于显示。PS3EYEDriver 附带了一个类似的示例,其中捕获数据的指针被传递到 SDL2 进行显示。作为一个练习,你可能想要调整这两个示例来构建一个集成 OpenCV 与 PS3EYEDriver 进行捕获和 SDL2 进行显示的演示。

希望经过一些实验后,你会得出结论,PlayStation Eye 相机比其 $10 的价格标签所暗示的更具有能力。对于快速移动的物体,它的高帧率是低分辨率的良好折衷。消除运动模糊!

如果我们愿意投资硬件修改,Eye 相机还有更多隐藏的技巧(或在其插座中)。镜头和红外阻挡滤光片相对容易更换。副厂镜头和滤光片可以允许进行近红外捕捉。此外,副厂镜头可以提供更高的分辨率、不同的视场角、更少的畸变和更高的效率。Peau Productions 不仅销售预修改的 Eye 相机,还提供 DIY 套件,详情请见 peauproductions.com/store/index.php?cPath=136_1。公司的修改支持带有 m12 或 CS 螺纹接口的互换镜头(两种不同的螺纹接口标准)。网站根据镜头特性(如畸变和红外传输)提供详细的推荐。Peau 的预修改近红外 Eye 相机加镜头的价格起价约为 $85。更昂贵的选项,包括畸变校正镜头,最高可达 $585。然而,在这些价格下,建议在多个供应商之间比较镜头价格,正如本章后面的 购物指南 部分所述。

接下来,我们将考察一款缺乏高速模式的相机,但设计用于分别捕捉可见光和近红外光,并带有主动近红外照明。

为 ASUS Xtion PRO Live 和其他 OpenNI 兼容的深度相机提供加速

ASUS 于 2012 年推出了 Xtion PRO Live,作为动作控制游戏、自然用户界面(NUI)和计算机视觉研究的输入设备。它是基于 PrimeSense 设计的传感器的六款类似相机之一,PrimeSense 是一家以色列公司,苹果公司在 2013 年收购并关闭了该公司。有关 Xtion PRO Live 与使用 PrimeSense 传感器的其他设备的简要比较,请参阅以下表格:

名称 价格和可用性 最高分辨率近红外模式 最高分辨率彩色模式 最高分辨率深度模式 深度范围
微软 Xbox 360 Kinect $135 可用 640x480 @ 30 FPS 640x480 @ 30 FPS 640x480 @ 30 FPS 0.8m 至 3.5m
ASUS Xtion PRO $200 已停售 1280x1024 @ 60 FPS 640x480 @ 30 FPS 0.8m 至 3.5m
ASUS Xtion PRO Live $230 可用 1280x1024 @ 60 FPS 1280x1024 @ 60 FPS 640x480 @ 30 FPS 0.8m 至 3.5m
PrimeSense Carmine 1.08 $300 已停售 1280x960 @ 60 FPS 1280x960 @ 60 FPS 640x480 @ 30 FPS 0.8m 至 3.5m
PrimeSense Carmine 1.09 $325 已停售 1280x960 @ 60 FPS 1280x960 @ 60 FPS 640x480 @ 30 FPS 0.35m 至 1.4m
结构传感器 $380 可用 640x480 @ 30 FPS 640x480 @ 30 FPS 0.4m 至 3.5m

所有这些设备都包含一个近红外 (NIR) 摄像头和近红外照明源。光源投射出近红外点图案,可能在 0.8m 到 3.5m 的距离内被检测到,具体取决于型号。大多数设备还包含一个 RGB 彩色摄像头。基于主动 NIR 图像(点图案)和被动 RGB 图像,设备可以估计距离并生成所谓的 深度图,包含 640x480 点的距离估计。因此,该设备最多有三种模式:NIR(摄像头图像)、彩色(摄像头图像)和深度(处理后的图像)。

注意

关于在深度成像中有用的主动照明或结构光类型的信息,请参阅以下论文:

David Fofi, Tadeusz Sliwa, Yvon Voisin, “A comparative survey on invisible structured light”, SPIE Electronic Imaging - Machine Vision Applications in Industrial Inspection XII, San José, USA, pp. 90-97, January, 2004.

论文可在以下网址在线获取:www.le2i.cnrs.fr/IMG/publications/fofi04a.pdf

Xtion、Carmine 和 Structure Sensor 设备以及某些版本的 Kinect 与名为 OpenNI 和 OpenNI2 的开源 SDK 兼容。OpenNI 和 OpenNI2 都可在 Apache 许可证下使用。在 Windows 上,OpenNI2 随带对许多相机的支持。然而,在 Linux 和 Mac 上,Xtion、Carmine 和 Structure Sensor 设备的支持是通过一个名为 PrimeSense Sensor 的额外模块提供的,该模块也是开源的,并遵循 Apache 许可证。Sensor 模块和 OpenNI2 有独立的安装程序,Sensor 模块必须首先安装。根据您的操作系统,从以下 URL 获取 Sensor 模块:

下载此存档后,解压缩它并运行解压缩文件夹内的 install.sh

注意

对于 Kinect 兼容性,尝试传感器模块的 SensorKinect 分支。SensorKinect 的下载可在github.com/avin2/SensorKinect/downloads找到。SensorKinect 仅支持 Xbox 360 的 Kinect,不支持型号 1473。(型号编号打印在设备底部。)此外,SensorKinect 仅与 OpenNI(而不是 OpenNI2)的老版本开发构建兼容。有关旧版 OpenNI 的下载链接,请参阅nummist.com/opencv/

现在,在任意操作系统上,我们需要从源代码构建 OpenNI2 的最新开发版本。(较旧、稳定的版本在 Xtion PRO Live 上不工作,至少在某些系统上。)源代码可以作为一个 ZIP 存档从github.com/occipital/OpenNI2/archive/develop.zip下载,或者可以使用以下命令将其作为 Git 仓库克隆:

$ git clone –b develop https://github.com/occipital/OpenNI2.git

让我们将解压后的目录或本地仓库目录称为<openni2_path>。此路径应包含 Windows 的 Visual Studio 项目和一个 Linux 或 Mac 的 Makefile。构建项目(使用 Visual Studio 或make命令)。库文件生成在如<openni2_path>/Bin/x64-Release<openni2_path>/Bin/x64-Release/OpenNI2/Drivers(或其他架构的类似名称)的目录中。在 Windows 上,将这些两个文件夹添加到系统的Path中,以便应用程序可以找到dll文件。在 Linux 或 Mac 上,编辑您的~/.profile文件,并添加以下类似的行以创建与 OpenNI2 相关的环境变量:

export OPENNI2_INCLUDE="<openni2_path>/Include"
export OPENNI2_REDIST="<openni2_path>/Bin/x64-Release"

到目前为止,我们已经设置了支持传感器模块的 OpenNI2,因此我们可以为 Xtion PRO Live 或其他基于 PrimeSense 硬件的相机创建应用程序。源代码、Visual Studio 项目和几个示例的 Makefiles 可以在<openni2_path>/Samples中找到。

注意

可选地,可以将 OpenCV 的 videoio 模块编译为支持通过 OpenNI 或 OpenNI2 捕获图像。然而,我们将直接从 OpenNI2 捕获图像,然后将其转换为与 OpenCV 一起使用。通过直接使用 OpenNI2,我们可以获得更多控制相机模式选择的能力,例如原始近红外捕获。

Xtion 设备是为 USB 2.0 设计的,它们的标准固件不与 USB 3.0 端口兼容。为了实现 USB 3.0 兼容性,我们需要一个非官方的固件更新。固件更新程序只能在 Windows 上运行,但更新应用后,设备在 Linux 和 Mac 上也能实现 USB 3.0 兼容。要获取并应用更新,请按照以下步骤操作:

  1. github.com/nh2/asus-xtion-fix/blob/master/FW579-RD1081-112v2.zip?raw=true下载更新,并将其解压到任何目标位置,我们将此位置称为<xtion_firmware_unzip_path>

  2. 确保 Xtion 设备已连接。

  3. 打开命令提示符并运行以下命令:

    > cd <xtion_firmware_unzip_path>\UsbUpdate
    > !Update-RD108x!
    
    

    如果固件更新器打印出错误,这些错误并不一定是致命的。请继续使用我们在此展示的演示应用程序测试相机。

为了了解 Xtion PRO Live 作为主动或被动 NIR 相机的功能,我们将构建一个简单的应用程序来捕获和显示设备中的图像。让我们称这个应用程序为 Infravision。

注意

Infravision 的源代码和构建文件位于本书 GitHub 仓库的github.com/OpenCVBlueprints/OpenCVBlueprints/tree/master/chapter_1/Infravision

此项目只需要一个源文件,即Infravision.cpp。从 C 标准库中,我们将使用格式化和打印字符串的功能。因此,我们的实现从以下导入语句开始:

#include <stdio.h>
#include <stdlib.h>

Infravision 将使用 OpenNI2 和 OpenCV。从 OpenCV 中,我们将使用核心和 imgproc 模块进行基本的图像处理,以及 highgui 模块进行事件处理和显示。以下是相关的导入语句:

#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <OpenNI.h>

注意

OpenNI2 以及 OpenNI 的文档可以在网上找到,地址为structure.io/openni

Infravision 中只有一个函数,即main函数。它从定义两个常量开始,我们可能需要配置这些常量。第一个常量指定通过 OpenNI 捕获的传感器数据类型。这可以是SENSOR_IR(红外相机的单色输出)、SENSOR_COLOR(彩色相机的 RGB 输出)或SENSOR_DEPTH(处理过的、反映每个点估计距离的混合数据)。第二个常量是应用程序窗口的标题。以下是相关的定义:

int main(int argc, char *argv[]) {

  const openni::SensorType sensorType = openni::SENSOR_IR;
//  const openni::SensorType sensorType = openni::SENSOR_COLOR;
//  const openni::SensorType sensorType = openni::SENSOR_DEPTH;
  const char windowName[] = "Infravision";

根据捕获模式,我们将定义相应的 OpenCV 矩阵的格式。红外和深度模式是单色的,位深为 16 位。彩色模式有三个通道,每个通道位深为 8 位,如下面的代码所示:

  int srcMatType;
  if (sensorType == openni::SENSOR_COLOR) {
    srcMatType = CV_8UC3;
  } else {
    srcMatType = CV_16U;
  }

让我们通过几个步骤来初始化 OpenNI2、连接到相机、配置它并开始捕获图像。以下是第一步的代码,初始化库:

  openni::Status status;

  status = openni::OpenNI::initialize();
  if (status != openni::STATUS_OK) {
    printf(
        "Failed to initialize OpenNI:\n%s\n",
        openni::OpenNI::getExtendedError());
    return EXIT_FAILURE;
  }

接下来,我们将连接到任何可用的 OpenNI 兼容相机:

  openni::Device device;
  status = device.open(openni::ANY_DEVICE);
  if (status != openni::STATUS_OK) {
    printf(
        "Failed to open device:\n%s\n",
        openni::OpenNI::getExtendedError());
    openni::OpenNI::shutdown();
    return EXIT_FAILURE;
  }

我们将通过尝试获取有关该传感器的信息来确保设备具有适当的传感器类型:

  const openni::SensorInfo *sensorInfo =
      device.getSensorInfo(sensorType);
  if (sensorInfo == NULL) {
    printf("Failed to find sensor of appropriate type\n");
    device.close();
    openni::OpenNI::shutdown();
    return EXIT_FAILURE;
  }

我们还将创建一个流,但尚未启动:

  openni::VideoStream stream;
  status = stream.create(device, sensorType);
  if (status != openni::STATUS_OK) {
    printf(
        "Failed to create stream:\n%s\n",
        openni::OpenNI::getExtendedError());
    device.close();
    openni::OpenNI::shutdown();
    return EXIT_FAILURE;
  }

我们将查询支持的视频模式,并遍历它们以找到具有最高分辨率的模式。然后,我们将选择此模式:

  // Select the video mode with the highest resolution.
  {
    const openni::Array<openni::VideoMode> *videoModes =
        &sensorInfo->getSupportedVideoModes();
    int maxResolutionX = -1;
    int maxResolutionIndex = 0;
    for (int i = 0; i < videoModes->getSize(); i++) {
      int resolutionX = (*videoModes)[i].getResolutionX();
      if (resolutionX > maxResolutionX) {
        maxResolutionX = resolutionX;
        maxResolutionIndex = i;
      }
    }
    stream.setVideoMode((*videoModes)[maxResolutionIndex]);
  }

我们将开始从相机流式传输图像:

  status = stream.start();
  if (status != openni::STATUS_OK) {
    printf(
        "Failed to start stream:\n%s\n",
        openni::OpenNI::getExtendedError());
    stream.destroy();
    device.close();
    openni::OpenNI::shutdown();
    return EXIT_FAILURE;
  }

为了准备捕获和显示图像,我们将创建一个 OpenNI 帧、一个 OpenCV 矩阵和一个窗口:

  openni::VideoFrameRef frame;
  cv::Mat dstMat;
  cv::namedWindow(windowName);

接下来,我们将实现应用程序的主循环。在每次迭代中,我们将通过 OpenNI 捕获一帧,将其转换为典型的 OpenCV 格式(要么是 8 bpp 的灰度,要么是每个通道 8 bpp 的 BGR),并通过 highgui 模块显示它。循环在用户按下任何键时结束。以下是实现代码:

  // Capture and display frames until any key is pressed.
  while (cv::waitKey(1) == -1) {
    status = stream.readFrame(&frame);
    if (frame.isValid()) {
      cv::Mat srcMat(
          frame.getHeight(), frame.getWidth(), srcMatType,
          (void *)frame.getData(), frame.getStrideInBytes());
      if (sensorType == openni::SENSOR_COLOR) {
        cv::cvtColor(srcMat, dstMat, cv::COLOR_RGB2BGR);
      } else {
        srcMat.convertTo(dstMat, CV_8U);
      }
      cv::imshow(windowName, dstMat);
    }
  }

注意

OpenCV 的高级 GUI 模块存在许多不足。它不允许处理标准退出事件,例如点击窗口的 X 按钮。因此,我们基于按键来退出。此外,highgui 在轮询事件(如按键)时至少会引入 1ms 的延迟(但可能更多,取决于操作系统在线程之间切换的最小时间)。对于演示低帧率相机(如具有 30 FPS 限制的 Xtion PRO Live)的目的,这种延迟不应产生影响。然而,在下一节“超级提升 GS3-U3-23S6M-C 和其他 Point Gray 研究相机”中,我们将探讨 SDL2 作为比 highgui 更高效的替代方案。

在循环结束(由于用户按下键)后,我们将清理窗口和所有 OpenNI 资源,如下面的代码所示:

  cv::destroyWindow(windowName);

  stream.stop();
  stream.destroy();
  device.close();
  openni::OpenNI::shutdown();
}

这标志着源代码的结束。在 Windows 上,Infravision 可以在 Visual Studio 中构建为 Visual C++ Win32 控制台项目。请记住右键单击项目并编辑其项目属性,以便C++ | 通用 | 附加包含目录列出 OpenCV 和 OpenNI 的 include 目录的路径。此外,编辑链接器 | 输入 | 附加依赖项,以便它列出 opencv_core300.libopencv_imgproc300.lib(或类似命名的 lib 文件,用于除 3.0.0 之外的其他 OpenCV 版本)以及 OpenNI2.lib 的路径。最后,确保 OpenCV 和 OpenNI 的 dll 文件位于系统的 Path 中。

在 Linux 或 Mac 上,可以使用以下终端命令(假设 OPENNI2_INCLUDEOPENNI2_REDIST 环境变量已按本节前面所述定义)编译 Infravision:

$ g++ Infravision.cpp -o Infravision \
 -I include -I $OPENNI2_INCLUDE -L $OPENNI2_REDIST \
 -Wl,-R$OPENNI2_REDIST -Wl,-R$OPENNI2_REDIST/OPENNI2 \
 -lopencv_core -lopencv_highgui -lopencv_imgproc -lOpenNI2

注意

-Wl,-R 标志指定了可执行文件在运行时搜索库文件的附加路径。

在构建 Infravision 后,运行它并观察 Xtion PRO Live 投射到附近物体上的 NIR 点的模式。当从远处的物体反射时,点稀疏分布,但当从附近的物体反射时,点密集分布或甚至难以区分。因此,点的密度是距离的预测指标。以下是显示在阳光照射的房间中效果的截图,其中 NIR 光来自 Xtion 和窗户:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00005.jpeg

或者,如果你想将 Xtion 用作一个被动近红外相机,只需覆盖住相机的近红外发射器。你的手指不会完全阻挡发射器的光线,但一块电工胶带可以。现在,将相机对准一个中等亮度近红外照明的场景。例如,Xtion 应该能够在阳光照射的房间里或夜晚篝火旁拍摄到良好的被动近红外图像。然而,相机在阳光明媚的户外场景中表现不佳,因为这与设备设计的条件相比要亮得多。以下是截图,显示了与上一个示例相同的阳光照射的房间,但这次 Xtion 的近红外发射器被覆盖了:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00006.jpeg

注意,所有的点都消失了,新的图像看起来像一张相对正常的黑白照片。然而,是否有任何物体看起来有奇怪的发光?

随意修改代码,使用SENSOR_DEPTHSENSOR_COLOR而不是SENSOR_IR。重新编译,重新运行应用程序,并观察效果。深度传感器提供深度图,在附近的区域看起来较亮,而在远处的区域或未知距离的区域看起来较暗。颜色传感器提供基于可见光谱的正常外观图像,如下面的同一阳光照射的房间截图所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00007.jpeg

比较前两个截图。注意,玫瑰的叶子在近红外图像中要亮得多。此外,在玫瑰下面的脚凳上的印刷图案在近红外图像中是不可见的。(当设计师选择颜料时,他们通常不会考虑物体在近红外光下的外观!)

可能你想将 Xtion 用作一个主动近红外成像设备——能够在短距离内进行夜视,但你不想看到近红外点的图案。只需用一些东西覆盖照明器以扩散近红外光,比如你的手指或一块布料。

作为这种扩散照明的例子,看看以下截图,显示了女性手腕的近红外图像:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00008.jpeg

注意,静脉在可见光中比在近红外光中更容易辨认。同样,主动近红外相机在捕捉人眼虹膜中可识别的细节方面具有优越的能力,如第六章中所示,利用生物特征进行高效人员识别。你能找到其他在近红外和可见光波长下看起来大不相同的事物的例子吗?

到目前为止,我们已经看到 OpenNI 兼容的相机可以通过编程和物理方式配置来捕捉多种类型的图像。然而,这些相机是为特定任务——室内场景中的深度估计——设计的,它们可能不适合其他用途,如户外近红外成像。接下来,我们将探讨一个更多样化、更可配置且更昂贵的相机系列。

为 GS3-U3-23S6M-C 和其他 Point Grey Research 相机提供超级加速

Point Grey Research(PGR),一家加拿大公司,制造具有各种功能的工业相机。以下表格列出了一些例子:

Family and Model Price Color Sensitivity Highest Res Mode Sensor Format and Lens Mount Interface Shutter
Firefly MVFMVU-03MTC-CS $275 彩色 752x480 @ 60 FPS 1/3"CS mount USB 2.0 全局
Firefly MVFMVU-03MTM-CS $275 由可见光产生的灰色 752x480 @ 60 FPS 1/3"CS mount USB 2.0 全局
Flea 3FL3-U3-88S2C-C $900 彩色 4096x2160 @ 21 FPS 1/2.5"C mount USB 3.0 具有全局复位功能
Grasshopper 3GS3-U3-23S6C-C $1,000 彩色 1920x1200 @ 162 FPS 1/1.2"C mount USB 3.0 全局
Grasshopper 3GS3-U3-23S6M-C $1,000 由可见光产生的灰色 1920x1200 @ 162 FPS 1/1.2"C mount USB 3.0 全局
Grasshopper 3GS3-U3-41C6C-C $1,300 彩色 2048x2048 @ 90 FPS 1"C mount USB 3.0 全局
Grasshopper 3GS3-U3-41C6M-C $1,300 由可见光产生的灰色 2048x2048 @ 90 FPS 1"C mount USB 3.0 全局
Grasshopper 3GS3-U3-41C6NIR-C $1,300 由近红外光产生的灰色 2048x2048 @ 90 FPS 1"C mount USB 3.0 全局
GazelleGZL-CL-22C5M-C $1,500 由可见光产生的灰色 2048x1088 @ 280 FPS 2/3"C mount Camera Link 全局
GazelleGZL-CL-41C6M-C $2,200 由可见光产生的灰色 2048x2048 @ 150 FPS 1"C mount Camera Link 全局

备注

要浏览更多 PGR 相机的功能,请查看公司提供的相机选择工具:www.ptgrey.com/Camera-selector。有关 PGR 相机中传感器的性能统计信息,请参阅公司发布的相机传感器评论系列出版物,例如在www.ptgrey.com/press-release/10545上发布的那些。

关于传感器格式和镜头安装的更多信息,请参阅本章后面的“选购镜头”部分。

PGR 的一些最新相机使用索尼 Pregius 品牌的传感器。这种传感器技术以其高分辨率、高帧率和效率的组合而著称,如 PGR 在其白皮书ptgrey.com/white-paper/id/10795中所述。例如,GS3-U3-23S6M-C(单色相机)和 GS3-U3-23S6C-C(彩色相机)使用名为索尼 IMX174 CMOS 的 Pregius 传感器。得益于传感器和快速的 USB 3.0 接口,这些相机能够以 1920x1200 @ 162 FPS 的帧率捕捉图像。

本节中的代码已在 GS3-U3-23S6M-C 相机上进行了测试。然而,它也应该适用于其他 PGR 相机。作为一个单色相机,GS3-U3-23S6M-C 使我们能够看到传感器的分辨率和效率的完整潜力,而不需要任何颜色滤镜。

GS3-U3-23S6M-C 与大多数 PGR 相机一样,不附带镜头;而是使用标准 C 型接口来安装可互换镜头。本章后面的购物指南部分将讨论这种接口的低成本镜头示例。

GS3-U3-23S6M-C 需要 USB 3.0 接口。对于台式电脑,可以通过 PCIe 扩展卡添加 USB 3.0 接口,这可能需要花费 15 到 60 美元。PGR 销售保证与其相机兼容的 PCIe 扩展卡;然而,我也使用过其他品牌并取得了成功。

一旦我们配备了必要的硬件,我们需要获取一个名为 FlyCapture2 的应用程序来配置和测试我们的 PGR 相机。与此应用程序一起,我们将获得 FlyCapture2 SDK,这是我们 PGR 相机所有功能的完整编程接口。请访问www.ptgrey.com/support/downloads并下载相关的安装程序。(如果您尚未注册用户账户,您将被提示注册。)在撰写本文时,相关的下载链接具有以下名称:

  • FlyCapture 2.8.3.1 SDK - Windows (64-bit)

  • FlyCapture 2.8.3.1 SDK - Windows (32-bit)

  • FlyCapture 2.8.3.1 SDK - Linux Ubuntu (64-bit)

  • FlyCapture 2.8.3.1 SDK - Linux Ubuntu (32-bit)

  • FlyCapture 2.8.3.1 SDK - ARM Hard Float

注意

PGR 不提供适用于 Mac 的应用程序或 SDK。然而,原则上,第三方应用程序或 SDK 可能能够使用 PGR 相机在 Mac 上运行,因为大多数 PGR 相机都符合 IIDC/DCAM 等标准。

对于 Windows,运行您下载的安装程序。如果不确定,当提示时选择完整安装。一个快捷方式,Point Grey FlyCap2,应该出现在您的开始菜单中。

对于 Linux,解压缩下载的存档。按照解压缩文件夹中的 README 文件中的安装说明进行操作。一个启动器,FlyCap2,应该出现在您的应用程序菜单中。

安装完成后,请插入您的 PGR 相机并打开应用程序。您应该会看到一个标题为FlyCapture2 相机选择的窗口,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00009.jpeg

确保您的相机已选中,然后点击配置所选按钮。应该会出现另一个窗口。其标题包括相机名称,例如Point Grey Research Grasshopper3 GS3-U3-23S6M。所有相机设置都可以在这个窗口中配置。我发现相机视频模式标签页特别有用。选择它。您应该会看到有关捕获模式、像素格式、裁剪区域(称为感兴趣区域ROI)和数据传输的选项,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00010.jpeg

关于可用模式和其它设置的更多信息,请参阅相机的技术参考手册,可以从www.ptgrey.com/support/downloads下载。请不要担心您可能会永久损坏任何设置;每次您拔掉相机时,它们都会重置。当您对设置满意时,点击应用并关闭窗口。现在,在相机选择窗口中,点击确定按钮。在 Linux 上,FlyCapture2 应用程序现在退出。在 Windows 上,我们应该看到一个新窗口,其标题栏中也包含相机的名称。此窗口显示实时视频流和统计数据。为了确保整个视频可见,选择菜单选项视图|拉伸以适应。现在,您应该会看到视频在窗口内以信封式显示,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00011.jpeg

如果视频看起来损坏(例如,如果您一次看到多个帧的片段),最可能的原因是主机计算机无法以足够高的速度处理数据传输。有两种可能的方法可以解决这个问题:

  • 我们可以传输更少的数据。例如,回到配置窗口的相机视频模式标签页,选择一个较小的感兴趣区域或分辨率较低的模式。

  • 我们可以配置操作系统和 BIOS,将处理传入数据任务的高优先级。有关详细信息,请参阅 PGR 的以下技术应用笔记(TAN):www.ptgrey.com/tan/10367

随意尝试 FlyCapture2 应用程序的其他功能,例如视频录制。完成后,关闭应用程序。

既然我们已经看到了 PGR 相机的实际应用,让我们编写自己的应用程序来以高速捕获和显示帧。它将支持 Windows 和 Linux。我们将把这个应用程序命名为 LookSpry。(“Spry”意味着敏捷、灵活或活泼,拥有这些特质的人被称为“看起来敏捷”。如果我们的高速相机应用程序是一个人,我们可能会这样描述它。)

注意

LookSpry 的源代码和构建文件可以在本书的 GitHub 仓库中找到,网址为 github.com/OpenCVBlueprints/OpenCVBlueprints/tree/master/chapter_1/LookSpry

与本章中我们的其他演示一样,LookSpry 可以在一个源文件 LookSpry.cpp 中实现。要开始实现,我们需要导入 C 标准库的一些功能,包括字符串格式化和计时:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

LookSpry 将使用三个额外的库:FlyCapture2 SDKFC2)、OpenCV 和 Simple DirectMedia Layer 2SDL2)。(SDL2 是用于编写多媒体应用的跨平台硬件抽象层。)从 OpenCV 中,我们将使用核心和 imgproc 模块进行基本的图像处理,以及 objdetect 模块进行人脸检测。在这个演示中,人脸检测的作用仅仅是展示我们可以使用高分辨率输入和高帧率执行真正的计算机视觉任务。以下是相关的导入语句:

#include <flycapture/C/FlyCapture2_C.h>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/objdetect.hpp>
#include <SDL2/SDL.h>

注意

FC2 是闭源软件,但 PGR 相机所有者可以获得使用它的许可证。库的文档可以在安装目录中找到。

SDL2 在 zlib 许可下是开源的。库的文档可以在网上找到,网址为 wiki.libsdl.org

在 LookSpry 的整个过程中,我们使用一个字符串格式化函数——要么是 Microsoft Visual C 库中的 sprintf_s,要么是标准 C 库中的 snprintf。对于我们的目的,这两个函数是等效的。我们将使用以下宏定义,以便在 Windows 上将 snprintf 映射到 sprintf_s

#ifdef _WIN32
#define snprintf sprintf_s
#endif

在几个点上,应用程序在调用 FlyCapture2 或 SDL2 中的函数时可能会遇到错误。这种错误应该在对话框中显示。以下两个辅助函数从 FC2 或 SDL2 获取并显示相关的错误消息:

void showFC2Error(fc2Error error) {
  if (error != FC2_ERROR_OK) {
    SDL_ShowSimpleMessage(SDL_MESSAGEBOX_ERROR,
            "FlyCapture2 Error",
            fc2ErrorToDescription(error), NULL);
  }
}

void showSDLError() {
  SDL_ShowSimpleMessageBox(
      SDL_MESSAGEBOX_ERROR, "SDL2 Error", SDL_GetError(), NULL);
}

LookSpry 的其余部分简单地实现在 main 函数中。在函数的开始,我们将定义几个可能需要配置的常量,包括图像捕获、人脸检测、帧率测量和显示的参数:

int main(int argc, char *argv[]) {

  const unsigned int cameraIndex = 0u;
  const unsigned int numImagesPerFPSMeasurement = 240u;
  const int windowWidth = 1440;
  const int windowHeight = 900;
  const char cascadeFilename[] = "haarcascade_frontalface_alt.xml";
  const double detectionScaleFactor = 1.25;
  const int detectionMinNeighbours = 4;
  const int detectionFlags = CV_HAAR_SCALE_IMAGE;
  const cv::Size detectionMinSize(120, 120);
  const cv::Size detectionMaxSize;
  const cv::Scalar detectionDrawColor(255.0, 0.0, 255.0);
  char strBuffer[256u];
  const size_t strBufferSize = 256u;

我们将声明一个图像格式,这将帮助 OpenCV 解释捕获的图像数据。(当开始捕获图像时,将为此变量分配一个值。)我们还将声明一个 OpenCV 矩阵,它将存储捕获图像的均衡、灰度版本。声明如下:

  int matType;
  cv::Mat equalizedGrayMat;

注意

均衡是一种对比度调整,它使输出图像中所有亮度级别都同样常见。这种调整使主题的外观在光照变化方面更加稳定。因此,在尝试检测或识别图像中的主题(如人脸)之前,通常会对图像进行均衡。

对于人脸检测,我们将创建一个CascadeClassifier对象(来自 OpenCV 的 objdetect 模块)。分类器加载一个级联文件,对于 Windows 系统,我们必须指定一个绝对路径;对于 Unix 系统,则指定一个相对路径。以下代码构建了路径、分类器和用于存储人脸检测结果的向量:

#ifdef _WIN32
  snprintf(strBuffer, strBufferSize, "%s/../%s", argv[0], cascadeFilename);
  cv::CascadeClassifier detector(strBuffer);
#else
  cv::CascadeClassifier detector(cascadeFilename);
#endif
  if (detector.empty()) {
    snprintf(strBuffer, strBufferSize, "%s could not be loaded.",
              cascadeFilename);
    SDL_ShowSimpleMessageBox(
      SDL_MESSAGEBOX_ERROR, "Failed to Load Cascade File", strBuffer,NULL);
    return EXIT_FAILURE;
  }
  std::vector<cv::Rect> detectionRects;

现在,我们必须设置与 FlyCapture2 相关的一些事情。首先,以下代码创建了一个图像头,它将接收捕获的数据和元数据:

  fc2Error error;

  fc2Image image;
  error = fc2CreateImage(&image);
  if (error != FC2_ERROR_OK) {
    showFC2Error(error);
    return EXIT_FAILURE;
  }

以下代码创建了一个 FC2 上下文,它负责查询、连接和从可用的摄像头捕获:

  fc2Context context;
  error = fc2CreateContext(&context);
  if (error != FC2_ERROR_OK) {
    showFC2Error(error);
    return EXIT_FAILURE;
  }

以下行使用上下文获取指定索引的摄像头的标识符:

  fc2PGRGuid cameraGUID;
  error = fc2GetCameraFromIndex(context, cameraIndex, &cameraGUID);
  if (error != FC2_ERROR_OK) {
    showFC2Error(error);
    return EXIT_FAILURE;
  }

我们连接到摄像头:

  error = fc2Connect(context, &cameraGUID);
  if (error != FC2_ERROR_OK) {
    showFC2Error(error);
    return EXIT_FAILURE;
  }

我们通过启动捕获会话来完成 FC2 变量的初始化:

  error = fc2StartCapture(context);
  if (error != FC2_ERROR_OK) {
    fc2Disconnect(context);
    showFC2Error(error);
    return EXIT_FAILURE;
  }

我们使用 SDL2 也需要几个初始化步骤。首先,我们必须加载库的主模块和视频模块,如下所示:

  if (SDL_Init(SDL_INIT_VIDEO) < 0) {
    fc2StopCapture(context);
    fc2Disconnect(context);
    showSDLError();
    return EXIT_FAILURE;
  }

接下来,在以下代码中,我们创建了一个具有指定标题和大小的窗口:

  SDL_Window *window = SDL_CreateWindow(
      "LookSpry", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
      windowWidth, windowHeight, 0u);
  if (window == NULL) {
    fc2StopCapture(context);
    fc2Disconnect(context);
    showSDLError();
    return EXIT_FAILURE;
  }

我们将创建一个渲染器,能够将纹理(图像数据)绘制到窗口表面。以下代码中的参数允许 SDL2 选择任何渲染设备和任何优化:

  SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, 0u);
  if (renderer == NULL) {
    fc2StopCapture(context);
    fc2Disconnect(context);
    SDL_DestroyWindow(window);
    showSDLError();
    return EXIT_FAILURE;
  }

接下来,我们将查询渲染器以查看 SDL2 选择了哪个渲染后端。可能包括 Direct3D、OpenGL 和软件渲染。根据后端,我们可能需要请求高质量的缩放模式,以便在缩放视频时不会出现像素化。以下是查询和配置渲染器的代码:

  SDL_RendererInfo rendererInfo;
  SDL_GetRendererInfo(renderer, &rendererInfo);

  if (strcmp(rendererInfo.name, "direct3d") == 0) {
    SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "best");
  } else if (strcmp(rendererInfo.name, "opengl") == 0) {
    SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear");
  }

为了向用户提供反馈,我们将在窗口标题栏中显示渲染后端的名称:

  snprintf(strBuffer, strBufferSize, "LookSpry | %s",
      rendererInfo.name);
  SDL_SetWindowTitle(window, strBuffer);

我们将声明与每一帧渲染的图像数据相关的变量。SDL2 使用纹理作为这些数据的接口:

  SDL_Texture *videoTex = NULL;
  void *videoTexPixels;
  int pitch;

我们还将声明与帧率测量相关的变量:

  clock_t startTicks = clock();
  clock_t endTicks;
  unsigned int numImagesCaptured = 0u;

另外三个变量将跟踪应用程序的状态——是否应该继续运行,是否应该检测人脸,以及是否应该镜像图像(水平翻转)以进行显示。以下是相关声明:

  bool running = true;
  bool detecting = true;
  bool mirroring = true;

现在,我们准备进入应用程序的主循环。在每次迭代中,我们轮询 SDL2 事件队列以获取任何事件。退出事件(例如,当点击窗口的关闭按钮时)会导致running标志被清除,并在迭代结束时退出main循环。当用户按下DM时,detectingmirroring标志将被反转。以下代码实现了事件处理逻辑:

  SDL_Event event;
  while (running) {
    while (SDL_PollEvent(&event)) {
      if (event.type == SDL_QUIT) {
        running = false;
        break;
      } else if (event.type == SDL_KEYUP) {
        switch(event.key.keysym.sym) {
        // When 'd' is pressed, start or stop [d]etection.
        case SDLK_d:
          detecting = !detecting;
          break;
        // When 'm' is pressed, [m]irror or un-mirror the video.
        case SDLK_m:
          mirroring = !mirroring;
          break;
        default:
          break;
        }
      }
    }

仍然在主循环中,我们尝试从摄像头获取下一张图像。以下代码以同步方式执行此操作:

    error = fc2RetrieveBuffer(context, &image);
    if (error != FC2_ERROR_OK) {
       fc2Disconnect(context);
       SDL_DestroyTexture(videoTex);
       SDL_DestroyRenderer(renderer);
       SDL_DestroyWindow(window);
       showFC2Error(error);
       return EXIT_FAILURE;
    }

小贴士

考虑到 GS3-U3-23S6M-C 和其他许多 Point Grey 相机的较高吞吐量,同步捕获在这里是合理的。图像来得如此之快,以至于我们可以预期在缓冲帧可用之前几乎没有或可以忽略不计的等待时间。因此,用户在事件处理过程中不会体验到任何可感知的延迟。然而,FC2 也提供了通过fc2SetCallbck函数的回调异步捕获。对于低吞吐量相机,这种异步选项可能更好,在这种情况下,捕获和渲染不会在事件轮询的同一循环中发生。

如果我们刚刚捕获了应用程序这次运行的第一帧,我们仍然需要初始化几个变量;例如,纹理是NULL。根据捕获图像的尺寸,我们可以设置均衡矩阵和渲染器(预缩放)缓冲区的大小,如下面的代码所示:

    if (videoTex == NULL) {
      equalizedGrayMat.create(image.rows, image.cols, CV_8UC1);
      SDL_RenderSetLogicalSize(renderer, image.cols, image.rows);

根据捕获图像的像素格式,我们可以选择与 OpenCV 矩阵和 SDL2 纹理紧密匹配的格式。对于单色捕获——以及我们假设为单色的原始捕获——我们将使用单通道矩阵和 YUV 纹理(具体来说,是 Y 通道)。以下代码处理相关情况:

      Uint32 videoTexPixelFormat;
      switch (image.format) {
        // For monochrome capture modes, plan to render captured data
        // to the Y plane of a planar YUV texture.
        case FC2_PIXEL_FORMAT_RAW8:
        case FC2_PIXEL_FORMAT_MONO8:
          videoTexPixelFormat = SDL_PIXELFORMAT_YV12;
          matType = CV_8UC1;
          break;

对于 YUV、RGB 或 BGR 格式的彩色捕获,我们将根据格式每像素的字节数选择匹配的纹理格式和矩阵通道数:

        // For color capture modes, plan to render captured data
        // to the entire space of a texture in a matching color
        // format.
        case FC2_PIXEL_FORMAT_422YUV8:
          videoTexPixelFormat = SDL_PIXELFORMAT_UYVY;
          matType = CV_8UC2;
          break;
        case FC2_PIXEL_FORMAT_RGB:
          videoTexPixelFormat = SDL_PIXELFORMAT_RGB24;
          matType = CV_8UC3;
          break;
        case FC2_PIXEL_FORMAT_BGR:
          videoTexPixelFormat = SDL_PIXELFORMAT_BGR24;
          matType = CV_8UC3;
          break;

一些捕获格式,包括每通道 16 bpp 的格式,目前在 LookSpry 中不受支持,被视为失败案例,如下面的代码所示:

        default:
          fc2StopCapture(context);
          fc2Disconnect(context);
          SDL_DestroyTexture(videoTex);
          SDL_DestroyRenderer(renderer);
          SDL_DestroyWindow(window);
                SDL_ShowSimpleMessageBox(
          SDL_MESSAGEBOX_ERROR,
          "Unsupported FlyCapture2 Pixel Format",
          "LookSpry supports RAW8, MONO8, 422YUV8, RGB, and BGR.",
          NULL);
          return EXIT_FAILURE;
      }

我们将创建一个具有给定格式和与捕获图像相同大小的纹理:

      videoTex = SDL_CreateTexture(
          renderer, videoTexPixelFormat, SDL_TEXTUREACCESS_STREAMING,
          image.cols, image.rows);
      if (videoTex == NULL) {
        fc2StopCapture(context);
        fc2Disconnect(context);
        SDL_DestroyRenderer(renderer);
        SDL_DestroyWindow(window);
        showSDLError();
        return EXIT_FAILURE;
      }

使用以下代码,让我们更新窗口标题栏以显示捕获图像和渲染图像的像素尺寸,以像素为单位:

      snprintf(
          strBuffer, strBufferSize, "LookSpry | %s | %dx%d --> %dx%d",
          rendererInfo.name, image.cols, image.rows, windowWidth,
          windowHeight);
      SDL_SetWindowTitle(window, strBuffer);
    }

接下来,如果应用程序处于人脸检测模式,我们将图像转换为均衡的灰度版本,如下面的代码所示:

    cv::Mat srcMat(image.rows, image.cols, matType, image.pData,
            image.stride);
    if (detecting) {
      switch (image.format) {
        // For monochrome capture modes, just equalize.
        case FC2_PIXEL_FORMAT_RAW8:
        case FC2_PIXEL_FORMAT_MONO8:
          cv::equalizeHist(srcMat, equalizedGrayMat);
          break;
        // For color capture modes, convert to gray and equalize.
        cv::cvtColor(srcMat, equalizedGrayMat,
               cv::COLOR_YUV2GRAY_UYVY);
          cv::equalizeHist(equalizedGrayMat, equalizedGrayMat);
          break;
        case FC2_PIXEL_FORMAT_RGB:
          cv::cvtColor(srcMat, equalizedGrayMat, cv::COLOR_RGB2GRAY);
          cv::equalizeHist(equalizedGrayMat, equalizedGrayMat);
          break;
        case FC2_PIXEL_FORMAT_BGR:
          cv::cvtColor(srcMat, equalizedGrayMat, cv::COLOR_BGR2GRAY);
          cv::equalizeHist(equalizedGrayMat, equalizedGrayMat);
          break;
        default:
          break;
      }

我们将在均衡图像上执行人脸检测。然后,在原始图像中,我们将围绕任何检测到的人脸绘制矩形:

      // Run the detector on the equalized image.
      detector.detectMultiScale(
          equalizedGrayMat, detectionRects, detectionScaleFactor,
          detectionMinNeighbours, detectionFlags, detectionMinSize,
          detectionMaxSize);
      // Draw the resulting detection rectangles on the original image.
      for (cv::Rect detectionRect : detectionRects) {
        cv::rectangle(srcMat, detectionRect, detectionDrawColor);
      }
    }

在这个阶段,我们已经完成了这一帧的计算机视觉任务,需要考虑我们的输出任务。图像数据将被复制到纹理中,然后进行渲染。首先,我们将锁定纹理,这意味着我们将获得对其内存的写入访问权。这通过以下 SDL2 函数调用完成:

    SDL_LockTexture(videoTex, NULL, &videoTexPixels, &pitch);

记住,如果相机处于单色捕获模式(或我们假设为单色的原始模式),我们正在使用 YUV 纹理。我们需要用中间值 128 填充 U 和 V 通道,以确保纹理是灰度的。以下代码通过使用 C 标准库中的memset函数有效地完成此操作:

    switch (image.format) {
    case FC2_PIXEL_FORMAT_RAW8:
    case FC2_PIXEL_FORMAT_MONO8:
      // Make the planar YUV video gray by setting all bytes in its U
      // and V planes to 128 (the middle of the range).
      memset(((unsigned char *)videoTexPixels + image.dataSize), 128,
             image.dataSize / 2u);
      break;
    default:
      break;
    }

现在,我们已经准备好将图像数据复制到纹理中。如果设置了mirroring标志,我们将同时复制和镜像数据。为了高效地完成这项任务,我们将目标数组包装在 OpenCV 的Mat中,然后使用 OpenCV 的flip函数同时翻转和复制数据。如果未设置mirroring标志,我们将简单地使用标准的 C memcpy函数复制数据。以下代码实现了这两种替代方案:

    if (mirroring) {
      // Flip the image data while copying it to the texture.
      cv::Mat dstMat(image.rows, image.cols, matType, videoTexPixels,
                     image.stride);
      cv::flip(srcMat, dstMat, 1);
    } else {
      // Copy the image data, as-is, to the texture.
      // Note that the PointGrey image and srcMat have pointers to the
      // same data, so the following code does reference the data that
      // we modified earlier via srcMat.
      memcpy(videoTexPixels, image.pData, image.dataSize);
    }

提示

通常,memcpy函数(来自 C 标准库)编译为块传输指令,这意味着它为复制大型数组提供了最佳可能的硬件加速。然而,它不支持在复制过程中对数据进行修改或重新排序。David Nadeau 的一篇文章对memcpy与四种其他复制技术进行了基准测试,每种技术使用四个编译器,可以在以下位置找到:nadeausoftware.com/articles/2012/05/c_c_tip_how_copy_memory_quickly

现在我们已经将帧的数据写入纹理,我们将解锁纹理(可能会将数据上传到 GPU),并告诉渲染器渲染它:

    SDL_UnlockTexture(videoTex);
    SDL_RenderCopy(renderer, videoTex, NULL, NULL);
    SDL_RenderPresent(renderer);

在指定数量的帧之后,我们将更新我们的 FPS 测量值,并在窗口标题栏中显示它,如下面的代码所示:

    numImagesCaptured++;
    if (numImagesCaptured >= numImagesPerFPSMeasurement) {
      endTicks = clock();
      snprintf(
        strBuffer, strBufferSize,
        "LookSpry | %s | %dx%d --> %dx%d | %ld FPS",
        rendererInfo.name, image.cols, image.rows, windowWidth,
        windowHeight,
        numImagesCaptured * CLOCKS_PER_SEC /
         (endTicks - startTicks));
      SDL_SetWindowTitle(window, strBuffer);
      startTicks = endTicks;
      numImagesCaptured = 0u;
    }
  }

应用程序的主循环中没有其他内容。一旦循环结束(由于用户关闭窗口),我们将清理 FC2 和 SDL2 资源并退出:

  fc2StopCapture(context);
  fc2Disconnect(context);
  SDL_DestroyTexture(videoTex);
  SDL_DestroyRenderer(renderer);
  SDL_DestroyWindow(window);
  return EXIT_SUCCESS;
}

在 Windows 上,可以在 Visual Studio 中将 LookSpry 构建为 Visual C++ Win32 控制台项目。请记住,右键单击项目并编辑其项目属性,以便C++ | 通用 | 附加包含目录列出 OpenCV 的、FlyCapture 2 的以及 SDL 2 的include目录的路径。同样,编辑链接器 | 输入 | 附加依赖项,以便它列出opencv_core300.libopencv_imgproc300.libopencv_objdetect300.lib(或 3.0.0 以外的其他 OpenCV 版本的类似命名的lib文件)以及FlyCapture2_C.libSDL2.libSDL2main.lib的路径。最后,确保 OpenCV 的dll文件在系统的Path中。

在 Linux 上,以下终端命令应该可以成功构建 LookSpry:

$ g++ LookSpry.cpp -o LookSpry `sdl2-config --cflags --libs` \
 -lflycapture-c -lopencv_core -lopencv_imgproc -lopencv_objdetect

确保 GS3-U3-23S6M-C 相机(或另一个 PGR 相机)已连接,并且已使用 FlyCap2 GUI 应用程序正确配置。请记住,每次拔掉相机时都会重置配置。

注意

FlyCap2 GUI 应用程序中的所有相机设置也可以通过 FlyCapture2 SDK 进行编程设置。请参阅 SDK 的官方文档和示例。

当你对相机的配置满意时,关闭 FlyCap2 GUI 应用程序并运行 LookSpry。通过按M键取消镜像或镜像视频,按D键停止或重新启动检测来尝试不同的图像处理模式。每种模式下每秒处理多少帧?检测模式中的帧率如何受人脸数量的影响?

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00012.jpeg

希望你已经观察到,在某些或所有模式下,LookSpry 处理帧的速度比典型显示器的 60Hz 刷新率快得多。如果我们在一个高质量的 144Hz 游戏显示器上观看实时视频,它看起来会更加平滑。然而,即使刷新率是瓶颈,我们仍然可以欣赏到这种实时视频的低延迟或响应速度。

由于 GS3-U3-23S6M-C 和其他 PGR 相机使用可互换的 C 口镜头,我们现在应该学习如何承担购买镜头的重大责任!

玻璃的采购

没有什么能比得上一件经过多年岁月考验且因有人呵护而继续闪耀的精心制作的玻璃制品。在我家的壁炉架上,我父母有一些这样的纪念品。其中之一是一朵小小的彩色玻璃花,它来自巴黎的一个购物中心,我和哥哥在我们第一次离家和家人的旅行中在那里吃了许多便宜但美味的饭菜。其他物品比我记得还要早。

我使用的一些二手镜头已有 30 或 40 年的历史;它们的制造国家已不复存在;然而,它们的玻璃和涂层仍然处于完美状态。我喜欢想这些镜头通过为前任主人拍摄许多精美的照片而赢得了如此好的照顾,也许它们在 40 年后仍然能为其他人拍摄精美的照片。

玻璃持久耐用。镜头设计也是如此。例如,蔡司 Planar T*镜头,这个行业中最受尊敬的品牌之一,其光学设计始于 19 世纪 90 年代,涂层工艺始于 20 世纪 30 年代。镜头增加了电子和电动组件以支持自动曝光、自动对焦和图像稳定。然而,光学和涂层的进化相对缓慢。从机械角度来看,许多老镜头都非常出色。

小贴士

色差和球差

理想镜头会使所有入射光线汇聚到一个焦点。然而,实际镜头存在像差,光线不会精确地汇聚到同一个点。如果不同颜色或波长的光线汇聚到不同的点,镜头就会产生色差,这在图像中表现为高对比度边缘周围的彩色光环。如果来自镜头中心和边缘的光线汇聚到不同的点,镜头就会产生球差,这在图像中表现为在失焦区域高对比度边缘周围的明亮光环。

从 20 世纪 70 年代开始,新的制造技术使得高端镜头在色差和球差方面的校正更好。阿波罗色差或“APO”镜头使用高折射率材料(通常是稀土元素)来校正色差。非球面或“ASPH”镜头使用复杂的曲线来校正球差。这些校正镜头通常更贵,但你应该留意它们,因为它们有时可能以优惠价格出现。

由于景深较浅,大光圈会产生最明显的像差。即使是非 APO 和非 ASPH 镜头,在大多数光圈设置和大多数场景下,像差可能微不足道。

通过一些讨价还价的技巧,我们可以用一只新镜头的价格找到六只 20 世纪 60 年代到 80 年代的好镜头。此外,即使是最近推出的镜头型号,在二手市场上也可能以 50%的折扣出售。在讨论具体优惠的例子之前,让我们考虑在购买任何二手镜头时可能遵循的五个步骤:

  1. 理解需求。正如我们在“捕捉瞬间的主题”部分之前讨论的那样,镜头和相机的许多参数相互作用,以确定我们是否能够拍摄到清晰及时的照片。至少,我们应该考虑适当的焦距、光圈数或 T 数,以及针对特定应用和相机的最近对焦距离。我们还应该尝试评估高分辨率和低畸变对特定应用的重要性。光的波长也很重要。例如,如果镜头针对可见光进行了优化(如大多数镜头那样),它可能并不一定高效地传输近红外光。

  2. 研究供应。如果你住在大城市,也许当地商家有大量价格低廉的二手镜头。否则,通常可以在 eBay 等拍卖网站上找到最低价格。根据我们在第一步中定义的需求进行搜索,例如,如果我们正在寻找 100mm 焦距和 2.8 的光圈数或 T 数,就搜索“100 2.8 镜头”。你找到的一些镜头可能没有与你的相机相同的类型。检查是否可以提供适配器。适配镜头通常是一个经济的选择,尤其是在长焦镜头的情况下,它们往往不会为小感光器的相机大量生产。创建一个似乎以有吸引力的价格满足要求的可用镜头型号的简短列表。

  3. 学习镜头模型。在线上,用户们发布了详细的规格、样本图像、测试数据、比较和意见吗?MFlenses (www.mflenses.com/) 是关于旧式手动对焦镜头的极好信息来源。它提供了许多评论和一个活跃的论坛。像 Flickr (www.flickr.com/) 这样的图片和视频托管网站也是寻找旧式和特殊镜头(包括电影镜头、夜视仪等)评论和样本输出的好地方!了解每种镜头模型在其制造年份中的变化。例如,某个镜头的早期版本可能是单层涂层的,而较新的版本则可能是多层涂层,以提高透射率、对比度和耐用性。

  4. 选择状况良好的物品。镜头应无霉斑或雾气。最好是没有划痕或清洁痕迹(涂层上的污点),尽管如果损坏在前镜片元素(离相机最远的元素)上,对图像质量的影响可能很小。

    镜头内部有一点灰尘不应影响图像质量。光圈和聚焦机制应平滑移动,最好是没有油光圈叶片。

  5. 参与竞标,出价,或者以卖家的价格购买。如果你认为竞标或谈判的价格过高,就为另一笔交易保存你的钱。记住,尽管卖家可能会告诉你,大多数廉价物品并不罕见,大多数廉价价格将会再次出现。继续寻找!

一些品牌因卓越的品质而享有持久的声誉。在 1860 年至 1930 年之间,卡尔·蔡司、莱卡和施耐德·克鲁斯纳赫等德国制造商巩固了他们作为优质光学产品创造者的名声。(施耐德·克鲁斯纳赫以电影镜头最为知名。)其他值得尊敬的欧洲品牌包括瑞士的 Alpa 和法国的 Angéniuex,它们都以其电影镜头最为知名。到 1950 年代,尼康开始获得认可,成为第一个能够与德国镜头质量相媲美的日本制造商。随后,富士和佳能成为电影和摄影相机高端镜头制造商。

尽管 Willy Loman(来自亚瑟·米勒的戏剧《推销员的死亡》)可能会建议我们购买“一个广受欢迎的机器”,但这并不一定是最好的交易。假设我们购买镜头是为了其实用价值而不是收藏价值,如果我们发现量产的无品牌镜头质量优秀,我们会很高兴。一些镜头相当接近这个理想。

东德和苏联大量生产了用于照相机、电影摄像机、投影仪、显微镜、夜视仪和其他设备的优质镜头。光学在从潜艇到宇宙飞船等重大项目中也同样重要!东德制造商包括卡尔·蔡司耶拿、迈耶和潘塔孔。苏联(后来是俄罗斯、乌克兰和白俄罗斯)制造商包括 KMZ、BelOMO、KOMZ、沃洛格达、LOMO、阿森纳等许多其他公司。通常,镜头设计和制造工艺在东欧集团的多家制造商中被复制和修改,赋予了这个地区和这个时代的镜头一个可识别的特征。

备注

东欧集团的一些镜头没有制造商的名称,因此它们是无名商品。一些型号,为了出口,只是标有“制造于苏联”或“aus JENA”(来自东德耶拿)。然而,大多数苏联镜头都带有符号和序列号,这些符号和序列号编码了制造地点和日期。有关这些标记的目录及其历史意义的描述,请参阅 Nathan Dayton 的文章“尝试消除迷雾”,见www.commiecameras.com/sov/

一些不太知名的日本品牌也倾向于是廉价的选择。例如,宾得制造出好的镜头,但它从未像其竞争对手那样享有同样的高端地位。公司的老款摄影镜头采用 M42 接口,这些镜头在低价位上特别丰富。此外,寻找公司的 C 接口电影镜头,这些镜头以前被品牌为 Cosmicar。

让我们看看如何将一些廉价镜头与 GS3-U3-23S6M-C 配合使用,以产生优质图像。记住,GS3-U3-23S6M-C 具有 C 接口和 1/1.2 英寸格式的传感器。对于 C 接口和 CS 接口的相机,传感器格式的名称并不指代传感器实际的任何尺寸!相反,由于历史原因,如 1/1.2 英寸这样的测量值指的是如果视频相机仍然使用真空管,那么真空管的直径!以下表格列出了常见传感器格式与传感器实际尺寸之间的转换:

格式名称 典型镜头接口 典型用途 对角线(毫米) 宽度(毫米) 高度(毫米) 宽高比
1/4 英寸 CS 机械视觉 4.0 3.2 2.4 4:3
1/3 英寸 CS 机械视觉 6.0 4.8 3.6 4:3
1/2.5 英寸 C 机械视觉 6.4 5.1 3.8 4:3
1/2 英寸 C 机械视觉 8.0 6.4 4.8 4:3
1/1.8 英寸 C 机械视觉 9.0 7.2 5.4 4:3
2/3 英寸 C 机械视觉 11.0 8.8 6.6 4:3
16 毫米 C 电影 12.7 10.3 7.5 4:3
1/1.2 英寸 C 机械视觉 13.3 10.7 8.0 4:3
超级 16 毫米 C 电影 14.6 12.5 7.4 5:3
1 英寸 C 机械视觉 16.0 12.8 9.6 4:3
四第三 四第三微四第三 摄影 电影 21.6 17.3 13.0
APS-C 诸如尼康 F 等各种专有 摄影 27.3 22.7 15.1 3:2
35mm(“全画幅”) M42 各种专有型号,如尼康 F 摄影 43.3 36.0 24.0 3:2

注意

有关机器视觉相机中典型传感器尺寸的更多信息,请参阅 Vision-Doctor.co.uk 的以下文章:www.vision-doctor.co.uk/camera-technology-basics/sensor-and-pixel-sizes.html

镜头投射出圆形图像,需要足够大的直径以覆盖传感器的对角线。否则,捕获的图像将出现暗角,意味着角落会模糊且暗。例如,在 Janet Howse 的画作《雪猴》的以下图像中,暗角很明显。该图像使用 1/1.2"传感器捕获,但镜头是为 1/2"传感器设计的,因此图像模糊且暗,除了中心的一个圆形区域:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00013.jpeg

为一个格式设计的镜头可以覆盖任何大小相似或更小的格式,而不会出现暗角;然而,我们可能需要一个适配器来安装镜头。系统的对角线视场角(以度为单位),取决于传感器的对角线大小和镜头的焦距,根据以下公式:

diagonalFOVDegrees =
  2 * atan(0.5 * sensorDiagonal / focalLength) * 180/pi

例如,如果一个镜头的焦距与传感器的对角线相同,其对角线视场角为 53.1 度。这样的镜头被称为标准镜头(相对于给定的传感器格式),53.1 度的视场角既不算宽也不算窄。

咨询上表,我们看到 1/1.2"格式与 16mm 和 Super 16 格式相似。因此,GS3-U3-23S6M-C(以及类似相机)应该能够使用大多数 C 口电影镜头而不会出现暗角,无需适配器,并且具有与镜头设计师意图相近的视场角。

假设我们想要一个窄视场角来拍摄远处的物体。我们可能需要一个长焦距,这在 C 口并不常见。比较对角线大小,注意 35mm 格式比 1/1.2"格式大 3.25 倍。当安装到 1/1.2"格式时,35mm 格式的标准镜头变成了具有 17.5 度视场角的长焦镜头!一个 M42 到 C 口的适配器可能需要 30 美元,并让我们可以使用大量的镜头!

假设我们想要拍摄近距离的物体,但我们的镜头都无法足够接近地聚焦。这个问题有一个低技术含量的解决方案——延长管,它可以增加镜头和相机之间的距离。对于 C 口,一套不同长度的几个延长管可能需要 30 美元。当镜头的总延伸(从管子加上其内置对焦机构)等于焦距时,物体以 1:1 的放大率投影到传感器上。例如,一个 13.3mm 的物体将填满 1/1.2"传感器 13.3mm 的对角线。以下公式成立:

magnificationRatio = totalExtension / focalLength

然而,为了获得高放大倍数,主题必须非常靠近前镜片元件才能对焦。有时,使用延伸筒会创建一个不切实际的光学系统,甚至无法对焦到其前镜片元件那么远!其他时候,主题的一部分和镜头外壳(如内置镜头遮光罩)可能会相互碰撞。可能需要进行一些实验,以确定镜头延伸的实际极限。

以下表格列出了我为 GS3-U3-23S6M-C 最近购买的某些镜头和镜头配件:

名称 价格 产地 座位和预期格式 焦距(mm) 1/1.2"传感器视野(度) 最大 f 数或 T 数
Cosmicar TV 镜头 12.5mm 1:1.8 $41 日本 1980 年代? CSuper 16 12.5 56.1 T/1.8
Vega-73 $35 LZOS 工厂利特卡里诺,苏联 1984 CSuper 16 20.0 36.9 T/2.0
Jupiter-11A $50 KOMZ 工厂喀山,苏联 1975 M4235mm 135.0 5.7 f/4.0
C 座延伸筒:10mm、20mm 和 40mm $30 日本新品 C - - -
Fotodiox M42 to C 适配器 $30 美国新品 M42 to C - - -

为了比较,请考虑以下表格,其中给出了专门为机器视觉设计的镜头的示例:

名称 价格 产地 座位和预期格式 焦距(mm) 1/1.2"传感器视野(度) 最大 f 数或 T 数
Fujinon CF12.5HA-1 $268 日本新品 C1" 12.5 56.1 T/1.4
Fujinon CF25HA-1 $270 日本新品 C1" 25.0 29.9 T/1.4
Fujinon CF50HA-1 $325 日本新品 C1" 50.0 15.2 T/1.8
Fujinon CF75HA-1 $320 日本新品 C1" 75.0 10.2 T/1.4

备注

上述表格中的价格来自 B&H(www.bhphotovideo.com),它是美国的主要摄影和视频供应商。它提供了一系列机器视觉镜头,通常比工业供应商列出的价格低。

这些新的机器视觉镜头可能非常出色。我还没有测试它们。然而,让我们看看我们选择的旧、二手、摄影和电影镜头的一些样本照片,它们的成本比这低六倍或更多。所有图像都是使用 FlyCapture2 以 1920x1200 的分辨率捕获的,但它们被调整大小以包含在这本书中。

首先,让我们尝试 Cosmicar 12.5mm 镜头。对于这种广角镜头,一个有许多直线的场景可以帮助我们判断畸变程度。以下样本照片显示了一个有许多书架的阅览室:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00014.jpeg

为了更好地看到细节,例如书籍背面的文本,请查看以下从图像中心裁剪的 480x480 像素(原始宽度的 25%)的图像:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00015.jpeg

以下图像是我眼睛血管的特写,使用 Vega-73 镜头和 10mm 延伸筒捕获:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00016.jpeg

再次强调,细节可以在中心 480x480 的裁剪中更好地欣赏,如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00017.jpeg

最后,让我们用 Jupiter-11A 捕捉一个远处的物体。这是在晴朗的夜晚的月亮:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00018.jpeg

再次,让我们检查图像中心的 480x480 裁剪,以了解镜头捕捉到的细节水平:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00019.jpeg

到目前为止,我们关于廉价镜头的讨论已经跨越了许多年代,从这里到月亮的距离!我鼓励你进一步探索。在给定的预算内,你能组装出哪些最有趣和功能强大的光学系统?

摘要

本章为我们提供了一个机会来考虑计算机视觉系统在硬件和软件层面的输入。此外,我们还解决了从输入阶段到处理和输出阶段高效调度图像数据的需求。我们的具体成就包括以下内容:

  • 理解不同波长的光和辐射之间的差异

  • 理解传感器、镜头和图像的各种特性

  • 使用 PlayStation Eye 相机和 OpenCV 的视频 io 模块捕捉和记录慢动作视频

  • 使用 ASUS Xtion PRO Live 相机和 OpenNI2 以及 OpenCV 的高 gui 模块比较可见光和近红外光下物体的外观

  • 使用 FlyCapture2、OpenCV、SDL2 和 GS3-U3-23S6M-C 相机在高速捕捉人脸的同时捕捉和渲染高清视频

  • 购买便宜、旧、二手的镜头,它们出奇地好!

接下来,在第二章《使用自动相机拍摄自然和野生动物》中,我们将随着构建一个捕捉和编辑自然环境纪录片片段的智能相机,加深我们对高质量图像的欣赏!

第二章. 使用自动相机拍摄自然和野生动物

国家地理以其野生动物的亲密照片而闻名。在杂志的页面上,动物常常显得比生命还要大,仿佛它们属于与背后的风景相同的“地理”尺度。这种放大效果可以通过使用广角镜头在非常近的距离上捕捉主题来实现。例如,史蒂夫·温特拍摄的一张难忘的照片显示了一只咆哮的老虎伸出爪子去击打镜头!

让我们考虑实现这种照片的可能方法。摄影师可以亲自跟踪一只野生老虎,但出于安全考虑,这种方法需要一定的距离和长焦镜头。近距离接触可能会危及人类、老虎或两者。或者,摄影师可以使用遥控车或无人机接近并拍摄老虎。这会更安全,但就像第一种技术一样,它很费力,一次只能覆盖一个地点,并且可能会因为吸引动物的注意而破坏即兴或自然照片的机会。最后,摄影师可以在老虎可能访问的多个地点部署隐蔽和自动化的相机,称为相机陷阱

本章将探讨编程相机陷阱的技术。也许我们不会捕捉到任何老虎,但总会有东西走进我们的陷阱!

尽管名字叫“陷阱”,但相机陷阱实际上并没有物理上“捕捉”任何东西。它只是在触发器被触发时捕捉照片。不同的相机陷阱可能使用不同的触发器,但在我们的案例中,触发器将是一个对运动、颜色或某些类别的物体敏感的计算机视觉系统。我们系统的软件组件将包括 OpenCV 3、Python 脚本、shell 脚本以及一个名为 gPhoto2 的相机控制工具。在构建我们的系统时,我们将解决以下问题:

  • 我们如何从主机计算机配置和触发照片相机?

  • 我们如何检测具有摄影价值的主题的存在?

  • 我们如何捕捉和处理一个主题的多个照片以创建一个有效的合成图像或视频?

注意

本章项目的所有脚本和数据都可以在本书的 GitHub 仓库中找到,网址为github.com/OpenCVBlueprints/OpenCVBlueprints/tree/master/chapter_2/CameraTrap

本章将重点介绍适用于类 Unix 系统(包括 Linux 和 Mac)的技术。我们假设用户最终将在低成本、低功耗的单板计算机(SBCs)上部署我们的相机陷阱,这些计算机通常运行 Linux 操作系统。一个好的例子是 Raspberry Pi 2 硬件,它通常运行 Raspbian 版本的 Linux。

让我们从我们的软件在图像捕获之前、期间和之后将执行的一些简单任务概述开始。

规划相机陷阱

我们将使用一台配备两个摄像头的计算机来设置相机陷阱。一个摄像头将连续捕捉低分辨率图像。例如,这个第一个摄像头可能是一个普通的网络摄像头。我们的软件将分析低分辨率图像以检测主体的存在。我们将探索基于运动、颜色和物体分类的三个基本检测技术。当检测到主体时,第二个摄像头将激活以捕捉和保存一系列有限的高分辨率图像。这个第二个摄像头将是一个专用的数码相机,拥有自己的电池和存储。我们不一定以最快的速度分析和记录图像;相反,我们将注意节约主机计算机的资源以及数码相机的电池功率和存储,以便我们的相机陷阱可以长时间运行。

可选地,我们的软件将为数码相机配置曝光分级。这意味着系列中的一些照片将被故意曝光不足,而其他照片将被过度曝光。稍后,我们将从相机上传照片到主机计算机,并合并曝光以产生高动态范围HDR)图像。这意味着合并后的照片将在比任何单一曝光能够捕捉的更广泛的阴影、中色调和高光范围内展示精细的细节和饱和的颜色。例如,以下阵容说明了曝光不足(左侧)、过度曝光(右侧)和合并的 HDR 照片(中间):

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00020.jpeg

HDR 成像在风景摄影中尤为重要。通常,天空比地面亮得多,但我们希望驯服这种对比度,以便在这两个区域都获得饱和的中色调颜色,而不是白色的无特征天空或黑色的无特征土地。我们还将探索将一系列图像转换为时间流逝视频的技术。

注意,本项目中的两个摄像头满足不同的需求。网络摄像头提供用于实时处理的图像流,而数码相机存储用于后期高质量处理的图像。考虑以下比较表:

功能 典型网络摄像头 典型数码相机 高端工业相机
价格 中等
功耗 高(但有自己的电池) 中等
配置选项
延迟
分辨率 非常高
耐用性 一般

可能,高端工业相机可以同时满足实时成像和高质量成像的双重目的。然而,一个网络摄像头和数码相机的组合可能更便宜。考虑以下例子:

名称 目的 传感器格式 最高分辨率模式 接口 价格
Point Grey Research Grasshopper 3 GS3-U3-120S6M-C 工业相机 1" 4242x2830 @ 7 FPS USB 3.0 $3,700(新品)
卡尔·蔡司耶拿 DDR Tevidon 10mm f/2 镜头 工业相机镜头 1" 清晰,适合高分辨率 300 美元(二手)
尼康 1 J5 配 10-30mm PD-ZOOM 镜头 照相机和镜头 1" 5568x3712 @ 20 FPS USB 2.0 500 美元(新品)
Odroid USB-Cam 720p Webcam 1/4" 1280x720 @ 30 FPS USB 2.0 20 美元(新品)

在这里,工业相机和镜头的成本是照相机、镜头和摄像头的八倍,但照相机应该提供最佳图像质量。尽管照相机具有 5568x3712 @ 20 FPS 的 捕获模式,请注意,其 USB 2.0 接口速度过慢,无法支持作为 传输模式。在列出的分辨率和速率下,照相机只能将图像记录到其本地存储。

对于我们的目的,照相机的主要弱点是高延迟。延迟不仅涉及电子设备,还包括移动的机械部件。为了减轻问题,我们可以采取以下步骤:

  • 使用比照相机视角略宽的摄像头。这样,相机陷阱可以提前检测到主题,并为照相机提供更多时间进行第一次拍摄。

  • 将照相机置于手动对焦模式,并将焦点设置在您计划拍摄主题的距离。手动对焦更快、更安静,因为自动对焦电机不会运行。

  • 如果您使用的是 数码单反相机DSLR),请将其置于 镜锁MLU)模式(如果支持 MLU)。如果没有 MLU,反射镜(将光线反射到光学取景器)必须在每次拍摄之前移出光学路径。使用 MLU 时,反射镜已经移开(但光学取景器已禁用)。MLU 更快、更安静、振动更小,因为反射镜不会移动。在某些相机上,MLU 被称为 实时视图,因为当光学取景器禁用时,数字(实时)取景器可能会被激活。

控制照相机是这个项目的重要组成部分。一旦你学会了编写摄影命令脚本,也许你将开始以新的方式思考摄影——因为它是一个过程,而不仅仅是快门落下的那一刻。现在让我们将注意力转向这个脚本主题。

使用 gPhoto2 控制 photo camera

gPhoto2 是一个开源、厂商中立的 Unix-like 系统(如 Linux 和 Mac)相机控制工具。它支持多个品牌的相机,包括佳能、尼康、奥林巴斯、宾得、索尼和富士。支持的功能因型号而异。以下表格列出了 gPhoto2 的主要功能,以及每个功能支持的官方相机数量:

功能 支持的设备数量 描述
文件传输 2105 在设备和设备之间传输文件
图像捕捉 489 使设备捕获图像到其本地存储
配置 428 更改设备的设置,例如快门速度
直播预览 309 从设备持续抓取实时视频帧

这些数字截至版本 2.5.8,是保守的。例如,一些配置功能在尼康 D80 上得到支持,尽管 gPhoto2 文档没有列出此相机为可配置。就我们的目的而言,图像捕获和配置是必需的功能,因此 gPhoto2 至少支持 428 款相机,也许还有更多。这个数字包括各种类型的相机,从便携式紧凑型到专业单反相机。

注意

要检查 gPhoto2 的最新版本是否官方支持特定相机的功能,请查看官方列表www.gphoto.org/proj/libgphoto2/support.php

通常,gPhoto2 通过 USB 使用名为图片传输协议PTP)的协议与相机通信。在继续之前,请检查您的相机是否有关于 PTP 模式的说明。您可能需要更改相机上的设置,以确保主机计算机将其视为 PTP 设备而不是 USB 存储设备。例如,在许多尼康相机上,必须选择设置菜单 | USB | PTP,如下面的图片所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00021.jpeg

此外,如果相机作为磁盘驱动器挂载,gPhoto2 无法与其通信。这有点问题,因为大多数操作系统都会自动将相机作为磁盘驱动器挂载,无论相机是否处于 PTP 模式。因此,在我们继续安装和使用 gPhoto2 之前,让我们看看如何通过编程方式卸载相机驱动器。

编写卸载相机驱动器的 shell 脚本

在 Mac 上,一个名为 PTPCamera 的进程负责代表 iPhoto 等应用程序挂载和控制相机。连接相机后,我们可以在终端中运行以下命令来终止 PTPCamera:

$ killall PTPCamera

然后,相机将可用于接收来自 gPhoto2 的命令。然而,请继续阅读,因为我们想编写支持 Linux 的代码!

在大多数桌面 Linux 系统上,当相机连接时,它将被挂载为Gnome 虚拟文件系统GVFS)卷。我们可以在终端中运行以下命令来列出挂载的 GVFS 卷:

$ gvfs-mount -l

例如,此命令在连接了尼康 D80 相机的 MacBook Pro 笔记本电脑(通过 USB 连接)的 Ubuntu 上产生以下输出:

Drive(0): APPLE SSD SM1024F
  Type: GProxyDrive (GProxyVolumeMonitorUDisks2)
  Volume(0): Recovery HD
    Type: GProxyVolume (GProxyVolumeMonitorUDisks2)
  Volume(1): Macintosh HD
    Type: GProxyVolume (GProxyVolumeMonitorUDisks2)
Drive(1): APPLE SD Card Reader
  Type: GProxyDrive (GProxyVolumeMonitorUDisks2)
Volume(0): NIKON DSC D80
  Type: GProxyVolume (GProxyVolumeMonitorGPhoto2)
  Mount(0): NIKON DSC D80 -> gphoto2://[usb:001,007]/
    Type: GProxyShadowMount (GProxyVolumeMonitorGPhoto2)
Mount(1): NIKON DSC D80 -> gphoto2://[usb:001,007]/
  Type: GDaemonMount

注意,输出包括相机的挂载点,在本例中为gphoto2://[usb:001,007]/。对于相机驱动器,GVFS 挂载点始终以gphoto2://开头。我们可以通过运行如下命令来卸载相机驱动器:

$ gvfs-mount –u gphoto2://[usb:001,007]/

现在,如果我们再次运行gvfs-mount -l,我们应该会看到相机不再列出。因此,它已卸载,应该可以接收来自 gPhoto2 的命令。

小贴士

或者,文件浏览器(如 Nautilus)将显示挂载的相机驱动器,并提供卸载它们的 GUI 控制。然而,作为程序员,我们更喜欢 shell 命令,因为它们更容易自动化。

我们需要在每次插入相机时都卸载相机。为了简化这个过程,让我们编写一个支持多个操作系统(Mac 或任何带有 GVFS 的 Linux 系统)和多个相机的 Bash shell 脚本。创建一个名为 unmount_cameras.sh 的文件,并填充以下 Bash 代码:

#!/usr/bin/env bash

if [ "$(uname)" == "Darwin" ]; then
  killall PTPCamera
else
  mounted_cameras=`gvfs-mount -l | grep -Po 'gphoto2://.*/' | uniq`
  for mounted_camera in $mounted_cameras; do
    gvfs-mount -u $mounted_camera
  done
fi

注意,此脚本检查操作系统的家族(其中 "Darwin" 是 Mac 的家族)。在 Mac 上,它运行 killall PTPCamera。在其他系统上,它使用 gvfs-mountgrepuniq 命令的组合来查找每个唯一的 gphoto2:// 挂载点,然后卸载所有相机。

让我们通过运行以下命令给脚本赋予“可执行”权限:

$ chmod +x unmount_cameras.sh

我们随时可以通过执行脚本来确保相机驱动器已卸载:

$ ./unmount_cameras.sh

现在,我们有一个标准的方式来使相机可用,因此我们准备安装和使用 gPhoto2。

设置和测试 gPhoto2

gPhoto2 及相关库在 Unix-like 系统的开源软件仓库中广泛可用。这并不奇怪——连接到数码相机是当今桌面计算中的常见任务!

对于 Mac,Apple 不提供包管理器,但第三方提供了。MacPorts 包管理器拥有最广泛的仓库。

注意

要设置 MacPorts 及其依赖项,请遵循官方指南 www.macports.org/install.php

要通过 MacPorts 安装 gPhoto2,请在终端运行以下命令:

$ sudo port install gphoto2

在 Debian 及其衍生版本中,包括 Ubuntu、Linux Mint 和 Raspbian,我们可以通过运行以下命令来安装 gPhoto2:

$ sudo apt-get install gphoto2

在 Fedora 及其衍生版本中,包括 Red Hat Enterprise Linux (RHEL) 和 CentOS,我们可以使用以下安装命令:

$ sudo yum install gphoto2

OpenSUSE 在 software.opensuse.org/package/gphoto 提供了 gPhoto2 的一键安装器。

安装 gPhoto2 后,让我们连接一个相机。确保相机已开启并处于 PTP 模式。然后,运行以下命令来卸载相机驱动器和拍照:

$ ./unmount_cameras.sh
$ gphoto2 --capture-image

如果相机处于自动对焦模式,你可能会看到或听到镜头移动。(确保相机有可观察的物体,以便自动对焦成功。否则,将不会捕捉到任何照片。)然后,你可能会听到快门打开和关闭。断开相机,使用其查看菜单浏览捕获的照片。如果那里有新照片,gPhoto2 正在运行!

要将相机中的所有图像上传到当前工作目录,我们可以重新连接相机并运行以下命令:

$ ./unmount_cameras.sh
$ gphoto2 --get-all-files

要了解 gphoto2 支持的所有标志,我们可以通过运行以下命令打开其手册:

$ man gphoto2

接下来,让我们尝试一个更高级的任务,涉及配置以及图像捕捉。我们将以曝光包围的方式拍摄一系列照片。

编写用于曝光包围的 shell 脚本

gPhoto2 提供了一个标志,--set-config,允许我们重新配置许多相机参数,包括曝光补偿。例如,假设我们想要通过相当于一整个光圈的等效值(加倍光圈面积或将其半径增加 sqrt(2) 倍)来过度曝光图像。这种偏差称为曝光补偿(或曝光调整)+1.0 曝光值EV)。以下命令配置相机使用 +1.0 EV,然后拍摄照片:

$ gphoto2 --set-config exposurecompensation=1000 --capture-image

注意,exposurecompensation 的值以千分之一 EV 计价,因此 1000 是 +1.0 EV。要欠曝,我们将使用负值。一系列具有不同 EV 的这些命令将实现曝光包围。

我们可以使用 --set-config 标志来控制许多摄影属性,而不仅仅是曝光补偿。例如,以下命令以一秒的曝光时间捕捉照片,同时以慢同步模式触发闪光灯:

$ gphoto2 --set-config shutterspeed=1s flashmode=2 --capture-image

以下命令列出了给定相机的所有支持属性和值:

$ gphoto2 --list-all-config

注意

关于光圈、曝光和其他摄影属性的进一步讨论,请参阅第一章,充分利用您的相机系统,特别是捕捉瞬间的主题部分。

在拍摄一系列曝光包围照片之前,将您的相机调至**光圈优先(A)**模式。这意味着光圈将保持不变,而快门速度将根据光线和 EV 值变化。恒定的光圈将有助于确保所有图像中的同一区域保持对焦。

让我们使用另一个 shell 脚本自动化曝光包围命令,我们将称之为 capture_exposure_bracket.sh。它将接受一个标志 -s,用于指定帧之间的曝光步长(以千分之一 EV 计),以及另一个标志 -f,用于指定帧数。默认值将是 3 帧,间隔为 1.0 EV。以下是脚本的实现:

#!/usr/bin/env bash

ev_step=1000
frames=3
while getopts s:f: flag; do
  case $flag in
    s)
      ev_step="$OPTARG"
      ;;
    f)
      frames="$OPTARG"
      ;;
    ?)
      exit
      ;;
  esac
done

min_ev=$((-ev_step * (frames - 1) / 2))
for ((i=0; i<frames; i++)); do
  ev=$((min_ev + i * ev_step))
  gphoto2 --set-config exposurecompensation=$ev \
    --capture-image
done
gphoto2 --set-config exposurecompensation=0

此脚本中的所有命令都适用于 Linux 和 Mac 的跨平台。请注意,我们正在使用 getopts 命令来解析参数,并使用 Bash 算术来计算每张照片的 EV 值。

记得通过运行以下命令给脚本赋予“可执行”权限:

$ chmod +x capture_exposure_bracket.sh

要卸载相机并每隔 1.5 EV 捕捉 5 张照片,我们可以运行以下命令:

$ ./unmount_cameras.sh
$ ./capture_exposure_bracket.sh –s 1500 –f 5

现在我们已经清楚地了解了如何从命令行控制相机,让我们考虑如何将此功能封装在一种通用编程语言中,该语言还可以与 OpenCV 交互。

编写用于封装 gPhoto2 的 Python 脚本

Python 是一种高级、动态的编程语言,拥有强大的第三方数学和科学库。OpenCV 的 Python 绑定既高效又相当成熟,封装了 C++ 库的所有主要功能,除了 GPU 优化。Python 也是一种方便的脚本语言,因为其标准库提供了跨平台的接口,可以访问系统的大部分功能。例如,编写 Python 代码来启动一个子进程(也称为子进程)很容易,它可以运行任何可执行文件,甚至是另一个解释器,如 Bash shell。

注意

有关从 Python 中启动和与子进程通信的更多信息,请参阅 subprocess 模块的文档,网址为 docs.python.org/2/library/subprocess.html。对于子进程是附加 Python 解释器的特殊情况,请参阅 multiprocessing 模块的文档,网址为 docs.python.org/2/library/multiprocessing.html

我们将使用 Python 的标准子进程功能来封装 gPhoto2 和我们自己的 shell 脚本。通过从子进程中发送相机命令,我们将使调用者(在 Python 中)将这些视为“发射并忘记”命令。也就是说,Python 进程中的函数会立即返回,这样调用者就不必等待相机处理命令。这是好事,因为相机通常需要几秒钟来自动对焦并捕获一系列照片。

让我们创建一个新的文件,CameraCommander.py,并从以下导入语句开始其实现:

import os
import subprocess

我们将编写一个名为 CameraCommander 的类。作为成员变量,它将有一个当前捕获过程(可能为 None)和一个日志文件。默认情况下,日志文件将是 /dev/null,这意味着日志输出将被丢弃。在设置成员变量之后,初始化方法将调用一个辅助方法来卸载相机驱动,以便相机准备好接收命令。以下是类的声明和初始化器:

class CameraCommander(object):

  def __init__(self, logPath=os.devnull):
    self._logFile = open(logPath, 'w')
    self._capProc = None
    self.unmount_cameras()

CameraCommander 的实例被删除时,它应该关闭日志文件,如下面的代码所示:

  def __del__(self):
    self._logFile.close()

每次打开 CameraCommander 的子进程时,命令应由 shell(Bash)解释,命令的打印输出和错误应重定向到日志文件。让我们在以下辅助方法中标准化子进程的这种配置:

  def _open_proc(self, command):
    return subprocess.Popen(
      command, shell=True, stdout=self._logFile,
      stderr=self._logFile)

现在,作为我们对 shell 命令的第一个包装器,让我们编写一个方法来在子进程中运行 unmount_cameras.sh。卸载相机驱动是一个短暂的过程,它必须在其他相机命令运行之前完成。因此,我们将实现我们的包装器方法,使其在 unmount_cameras.sh 返回之前不返回。也就是说,在这种情况下,子进程将以同步方式运行。以下是包装器实现的代码:

  def unmount_cameras(self):
    proc = self._open_proc('./unmount_cameras.sh')
    proc.wait()

接下来,让我们考虑如何捕获单个图像。我们首先将调用一个辅助方法来停止任何之前的、冲突的命令。然后,我们将使用通常的--capture-image标志调用gphoto2命令。以下是包装方法的实现:

  def capture_image(self):
    self.stop_capture()
    self._capProc = self._open_proc(
      'gphoto2 --capture-image')

作为另一种捕获模式,我们可以调用gphoto2来记录延时摄影系列。-I--interval标志,带有一个整数值,指定帧之间的延迟,单位为秒。-F--frames标志也接受一个整数值,指定系列中的帧数。如果使用了-I标志但省略了-F,则过程会无限期地捕获帧,直到被迫终止。让我们提供以下用于延时功能的包装器:

  def capture_time_lapse(self, interval, frames=0):
    self.stop_capture()
    if frames <= 0:
      # Capture an indefinite number of images.
      command = 'gphoto2 --capture-image -I %d' % interval
    else:
      command = 'gphoto2 --capture-image -I %d -F %d' %\
        (interval, frames)
    self._capProc = self._open_proc(command)

在拍摄一系列延时摄影照片之前,你可能需要将你的相机调整到手动曝光M)模式。这意味着光圈和快门速度将保持恒定,使用手动指定的值。假设场景的光线水平大致恒定,恒定的曝光将有助于防止延时视频中出现不愉快的闪烁。另一方面,如果我们预计在延时摄影系列过程中光线条件会有很大变化,那么 M 模式可能不合适,因为这些情况下,它会导致一些帧曝光不足,而其他帧则曝光过度。

为了允许曝光包围,我们可以简单地包装我们的capture_exposure_bracket.sh脚本,如下面的代码所示:

  def capture_exposure_bracket(self, ev_step=1.0, frames=3):
    self.stop_capture()
    self._capProc = self._open_proc(
      './capture_exposure_bracket.sh -s %d -f %d' %\
        (int(ev_step * 1000), frames))

正如我们在前三个方法中看到的,在尝试启动另一个之前终止任何正在进行的捕获过程是合理的。(毕竟,相机一次只能处理一个命令)。此外,调用者可能有其他原因要终止捕获过程。例如,主题可能已经离开。我们将提供以下方法来强制终止任何正在进行的捕获过程:

  def stop_capture(self):
    if self._capProc is not None:
      if self._capProc.poll() is None:
        # The process is currently running but might finish
        # before the next function call.
        try:
          self._capProc.terminate()
        except:
          # The process already finished.
          pass
      self._capProc = None

同样,我们将提供以下方法来等待任何当前正在运行的捕获过程的完成:

  def wait_capture(self):
    if self._capProc is not None:
      self._capProc.wait()
      self._capProc = None

最后,我们将提供以下属性获取器,以便调用者可以检查是否有一个捕获过程目前正在运行:

  @property
  def capturing(self):
    if self._capProc is None:
      return False
    elif self._capProc.poll() is None:
      return True
    else:
      self._capProc = None
      return False

这就完成了CameraCommander模块。为了测试我们的工作,让我们编写另一个脚本test_camera_commands.py,其实现如下:

#!/usr/bin/env python

import CameraCommander

def main():

  cc = CameraCommander.CameraCommander('test_camera_commands.log')

  cc.capture_image()
  print('Capturing image...')
  cc.wait_capture()
  print('Done')

  cc.capture_time_lapse(3, 2)
  print('Capturing 2 images at time interval of 3 seconds...')
  cc.wait_capture()
  print('Done')

  cc.capture_exposure_bracket(1.0, 3)
  print('Capturing 3 images at exposure interval of 1.0 EV...')
  cc.wait_capture()
  print('Done')

if __name__ == '__main__':
  main()

确保你的相机已开启,处于 PTP 模式,并且已连接。然后,使测试脚本可执行并运行,如下所示:

$ chmod +x test_camera_commands.py
$ ./test_camera_commands.py

等待所有命令完成,然后断开相机以查看图像。检查每张照片的时间戳和 EV 值。理想情况下,应该已经捕获了总共六张照片。然而,实际数量可能会根据诸如自动对焦的成功或失败、捕获和保存每张图像所花费的时间等因素而有所不同。如果有任何疑问,请使用文本编辑器查看日志文件test_camera_commands.log

查找 libgphoto2 和包装器

作为使用 gPhoto2 命令行工具的替代方案,我们可以使用底层的 C 库,libgphoto2 (github.com/gphoto/libgphoto2)。该库有几个第三方包装器,包括一组最新的 Python 绑定,称为 python-gphoto2 (github.com/gphoto/libgphoto2-python)。

OpenCV 3 的 videoio 模块对 libgphoto2 有可选支持。要启用此功能,我们可以使用WITH_GPHOTO2 CMake 定义从源代码配置和构建 OpenCV。当然,为了使此选项工作,系统必须已经安装了 libgphoto2 及其头文件。例如,在 Debian、Ubuntu、Linux Mint、Raspbian 和类似系统上,可以通过以下命令安装:

$ sudo apt-get install libgphoto2-dev

对于我们的目的,通过 libgphoto2 或 OpenCV 的 videoio 模块控制相机是过度的。我们不想抓取帧进行实时处理。我们只想让我们的 Python 脚本启动额外的进程来卸载和配置相机,并使其捕获照片到其本地存储。gPhoto2 命令行工具和我们的 shell 脚本作为子进程使用非常方便,因此我们将继续在本章的其余部分依赖它们。

注意

OpenCV 的一个官方示例演示了通过 videoio 模块使用 gPhoto2 兼容的相机。具体来说,该示例处理焦点控制。请参阅 OpenCV 的 GitHub 仓库中的源代码:github.com/Itseez/opencv/blob/master/samples/cpp/autofocus.cpp

检测有摄影价值主题的存在

第一章,《充分利用您的相机系统》,提出了一幅照片应该捕捉一个主题在某个时刻。让我们在寻找检测理想或“有摄影价值”的主题和时刻的方法时进一步探讨这个概念。

作为一种媒介,摄影利用光线、光圈、感光表面和时间来描绘场景的图像。最早期的摄影技术,在 19 世纪 20 年代,缺乏分辨率和速度,无法在精确的时刻传达一个详细的主题,但它能够捕捉到晴朗日子里模糊的场景。后来,随着更好的镜头、闪光灯和感光表面,摄影能够捕捉到清晰的场景、正式的肖像、更快更自然的肖像,最终是动作的瞬间,被冻结在时间中。

考虑以下一系列著名的照片,时间跨度从 1826 年到 1942 年:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00022.jpeg

为了一般兴趣,以下是关于前面照片的一些细节:

  • 右上角:勒格拉的窗景是历史上最早存活的照片,由尼埃普斯在 1826 年或 1827 年在法国圣洛佩-德-瓦雷涅斯拍摄。场景包括尼埃普斯庄园的部分屋顶和乡村。

  • 中左:路易·达盖尔在 1838 年拍摄的特姆普尔大道被认为是第一张包含人的照片。场景是巴黎的一条繁忙街道,但由于照片的慢速,大多数路人都是不可见的。在街角附近,一个男人正在擦另一个男人的靴子,所以这两个人在一个地方待了足够长的时间,可以被记录下来。

  • 右上角:让-巴蒂斯特·萨巴蒂耶-布洛特在 1844 年捕捉了路易·达盖尔的这张正式肖像。

  • 左下角:彩色摄影先驱谢尔盖·普罗库丁-戈尔斯基在 1910 年捕捉了这张相对随意的俄罗斯卡斯利工厂工人的肖像。照片中的男子正在卡斯利铁工厂制作铸件,该工厂在 19 世纪和 20 世纪初生产雕塑和豪华家具。

  • 左下角:马克斯·阿尔伯特在 1942 年 7 月 12 日,在卢甘斯克(今天在乌克兰)附近拍摄了这张战斗照片。主题是阿列克谢·戈尔杰耶维奇·耶尔缅科,他是红军的一名 23 岁初级政治军官。在照片的瞬间,耶尔缅科正在召集他的部队进攻。几秒钟后,他被击毙。

即使从这些少数例子中,我们也可以推断出一种历史趋势,即更动态的图像,这些图像捕捉了活动、变化甚至暴力的氛围。让我们在自然和野生动物摄影的背景下思考这一趋势。彩色摄影大约在 1907 年开始进入公众视野,经过几十年的技术改进,它成为比黑白更受欢迎的格式。色彩是动态的。风景、植物甚至动物的颜色会根据季节、天气、一天中的时间和它们的年龄而变化。今天,看到一部黑白自然纪录片似乎会很奇怪。

镜头技术的变化也对自然和野生动物摄影产生了深远的影响。随着更长、更快、更锐利的镜头,摄影师能够从远处窥视野生动物的生活。例如,今天,纪录片中充满了捕食者追逐猎物的场景。要拍摄这些场景,使用 20 世纪 20 年代的镜头将是困难的,甚至是不可能的。同样,微距(特写)镜头的质量也得到了很大提高,这对昆虫和其他小型生物的纪录片工作是一个福音。

最后,正如我们在本章开头的讨论中提到的,自动化技术的进步使得摄影师能够在偏远荒野、行动中部署相机。借助数字技术,远程相机可以存储大量的照片,并且这些照片可以轻松组合,产生效果,如时间流逝(强调运动)或 HDR(强调色彩)。如今,这些技术已被广泛使用,因此纪录片爱好者可能熟悉时间流逝的花朵从地面迅速生长或时间流逝的云朵在饱和的 HDR 天空中疾驰的景象。无论大小,一切都被描绘成动态的。

我们可以设计一些简单的规则来帮助区分动态场景和静态场景。以下是一些有用的线索:

  • 运动:我们可以假设场景中的任何运动都代表捕捉主题在行动或变化时刻的机会。无需知道主题是什么,我们就可以检测其运动并捕捉照片。

  • 颜色:我们可以假设在特定环境中某些颜色模式是不寻常的,并且它们出现在动态情况下。无需确切知道多彩的主题是什么,我们就可以检测其存在并拍摄它。例如,一大片新颜色可能是云层散开时的日落,或者花朵开放时的景象。

  • 分类:我们可以假设某些类型的主题是活着的,并且会与它们的环境互动,从而为动态照片创造机会。当我们检测到某个特定的主题类别时,我们可以通过拍照来响应。例如,我们将检测并拍摄哺乳动物的面部。

无论采用何种检测主题的方法,我们必须确保我们的网络摄像头和照相机对场景有相似的观点。它们应该指向同一目标。网络摄像头的视野角度应该与照相机一样宽,甚至更宽,以便网络摄像头能够在照相机视野之前检测到主题。两个摄像头都应该固定牢固,以防止由于振动、风或其他典型干扰而错位。例如,照相机可以安装在坚固的三脚架上,而网络摄像头可以贴在照相机的热靴上(通常为外部闪光灯或外部取景器预留的插槽)。以下图像显示了这种设置的示例:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00023.jpeg

为了提供背景,以下图像是同一设置的稍微远一点的视角。注意,网络摄像头和照相机指向同一主题:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00024.jpeg

我们将实现每种类型的相机陷阱作为单独的脚本,该脚本将接受命令行参数以调整陷阱的灵敏度。首先,让我们开发一个运动敏感的陷阱。

检测移动主题

我们的运动感应摄像头陷阱将依赖于我们之前在编写用于封装 gPhoto2 的 Python 脚本部分中实现的CameraCommander模块。此外,我们将使用 OpenCV 和 NumPy 来捕捉和分析网络摄像头图像。最后,从 Python 的标准库中,我们将导入argparse模块,它将帮助我们解析命令行参数,以及time模块,我们将用它来控制检测尝试之间的时间延迟。让我们创建一个文件,set_motion_trap.py,并从以下导入开始其实现:

#!/usr/bin/env python

import argparse
import time

import numpy
import cv2

import CameraCommander

此脚本将具有简单的结构,只有一个main()函数,它读取命令行参数并在循环中进行运动检测。一些参数与网络摄像头的使用有关,我们将称之为检测摄像头。其他参数涉及运动检测算法和相机的使用。main()函数开始于以下命令行参数的定义:

def main():

  parser = argparse.ArgumentParser(
    description='This script detects motion using an '
                'attached webcam. When it detects '
                'motion, it captures photos on an '
                'attached gPhoto2-compatible photo '
                'camera.')

  parser.add_argument(
    '--debug', type=bool, default=False,
    help='print debugging information')

  parser.add_argument(
    '--cam-index', type=int, default=-1,
    help='device index for detection camera '
         '(default=0)')
  parser.add_argument(
    '--width', type=int, default=320,
    help='capture width for detection camera '
         '(default=320)')
  parser.add_argument(
    '--height', type=int, default=240,
    help='capture height for detection camera '
         '(default=240)')
  parser.add_argument(
    '--detection-interval', type=float, default=0.25,
    help='interval between detection frames, in seconds '
         '(default=0.25)')

  parser.add_argument(
    '--learning-rate', type=float, default=0.008,
    help='learning rate for background subtractor, which '
         'is used in motion detection (default=0.008)')
  parser.add_argument(
    '--min-motion', type=float, default=0.15,
    help='proportion of frame that must be classified as '
         'foreground to trigger motion event '
         '(default=0.15, valid_range=[0.0, 1.0])')

  parser.add_argument(
    '--photo-count', type=int, default=1,
    help='number of photo frames per motion event '
         '(default=1)')
  parser.add_argument(
    '--photo-interval', type=float, default=3.0,
    help='interval between photo frames, in seconds '
         '(default=3.0)')
  parser.add_argument(
    '--photo-ev-step', type=float, default=None,
    help='exposure step between photo frames, in EV. If '
         'this is specified, --photo-interval is ignored '
         'and --photo-count refers to the length of an '
         'exposure bracketing sequence, not a time-lapse '
         'sequence.')

注意

当我们运行带有-h--help标志的脚本时,将显示参数的help文本,如下所示:

$ ./set_motion_trap.py -h

到目前为止,我们只声明了参数。接下来,我们需要解析它们并访问它们的值,如下面的代码所示:

  args = parser.parse_args()

  debug = args.debug

  cam_index = args.cam_index
  w, h = args.width, args.height
  detection_interval = args.detection_interval

  learning_rate = args.learning_rate
  min_motion = args.min_motion

  photo_count = args.photo_count
  photo_interval = args.photo_interval
  photo_ev_step = args.photo_ev_step

除了参数之外,我们还将使用几个变量。一个VideoCapture对象将使我们能够配置并从网络摄像头进行捕捉。矩阵(在 OpenCV 的 Python 包装器中实际上是 NumPy 数组)将使我们能够存储每个网络摄像头的 BGR 和灰度版本,以及一个前景掩码。前景掩码将由运动检测算法输出,它将是一个灰度图像,前景(移动)区域为白色,阴影区域为灰色,背景区域为黑色。具体来说,在我们的案例中,运动检测器将是 OpenCV 的BackgroundSubtractorMOG2类的实例。最后,我们需要一个CameraCommander类的实例来控制相机。以下是相关变量的声明:

  cap = cv2.VideoCapture(cam_index)
  cap.set(cv2.CAP_PROP_FRAME_WIDTH, w)
  cap.set(cv2.CAP_PROP_FRAME_HEIGHT, h)

  bgr = None
  gray = None
  fg_mask = None

  bg_sub = cv2.createBackgroundSubtractorMOG2()

  cc = CameraCommander.CameraCommander()

main() 函数实现的剩余部分是一个循环。在每次迭代中,我们将线程休眠一个指定的间隔(默认为 0.25 秒),因为这将节省系统资源。因此,我们将跳过一些网络摄像头的帧,但我们可能不需要完整的帧率来检测主题。如果我们没有设置休眠期,摄像头陷阱可能会一直使用 100%的 CPU 核心,尤其是在低功耗 SBC 上的慢速 CPU。以下是循环实现的第一部分:

  while True:
    time.sleep(detection_interval)

当我们读取帧时,我们将将其转换为灰度并均衡化:

    success, bgr = cap.read(bgr)
    if success:
      gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY, gray)
      gray = cv2.equalizeHist(gray, gray)

我们将把均衡后的帧和前景掩码传递给BackgroundSubtractorMOG2apply方法。此方法累积帧的历史记录,并根据历史记录中帧之间的差异来估计每个像素是否是前景区域、阴影或背景区域的一部分。作为第三个参数,我们将传递一个学习率,它是一个范围在[0.0, 1.0]之间的值。低值意味着将更多地考虑旧帧,因此估计将缓慢变化。看看我们如何在以下代码行中调用该方法:

      fg_mask = bg_sub.apply(gray, fg_mask, learning_rate)

注意

注意,在背景减法算法(如 MOG2)中,前景被定义为像素值在最近历史中发生变化的区域。相反,背景是像素值没有变化的区域。阴影指的是前景的阴影。有关 MOG2 和其他 OpenCV 支持的背景减法算法的详细信息,请参阅官方文档docs.opencv.org/3.0-beta/modules/video/doc/motion_analysis_and_object_tracking.html#backgroundsubtractormog2

作为背景减法器输入和输出的示例,考虑以下图像对。上面的图像是视频的 RGB 帧,而下面的图像是基于视频的前景掩码。请注意,场景是一个岩石海岸,前景有波浪拍打,远处有船只经过:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00025.jpeg

通过计算前景掩码中的白色(前景)值,我们可以得到摄像头在最近历史中捕获的运动量的粗略测量值。我们应该根据帧中的像素数量来归一化这个数值。以下是相关代码:

      h, w = fg_mask.shape
      motion = numpy.sum(numpy.where(fg_mask == 255, 1, 0))
      motion /= float(h * w)

如果脚本以--debug标志运行,我们将打印运动测量值:

      if debug:
        print('motion=%f' % motion)

如果运动超过指定的阈值,并且如果我们还没有开始捕获照片,我们现在将开始捕获照片。根据命令行参数,我们可能捕获曝光分级的系列或时间间隔系列,如下面的代码块所示:

      if motion >= min_motion and not cc.capturing:
        if photo_ev_step is not None:
          cc.capture_exposure_bracket(photo_ev_step, photo_count)
        else:
          cc.capture_time_lapse(photo_interval, photo_count)

在这里,循环和main()函数结束了。为了确保在执行脚本时main()运行,我们必须在脚本中添加以下代码:

if __name__ == '__main__':
  main()

我们可以给这个 Python 脚本赋予“可执行”权限,然后像其他 shell 脚本一样运行它,如下面的示例所示:

$ chmod +x set_motion_trap.py
$ ./set_motion_trap.py --debug True

考虑下面的图像对。左边的图像显示了运动激活的相机陷阱的物理设置,它恰好运行了带有默认参数的set_motion_trap.py。右边的图像是结果照片之一:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00026.jpeg

这些图像是用两个不同的相机拍摄的,因此它们在颜色和对比度上有所不同。然而,它们代表的是同一个场景。

尝试调整可选参数,以查看哪些设置对特定相机和移动主体类型最为有效。一旦我们了解了这个相机陷阱的敏感性,让我们继续进行另一个设计,使用一组颜色值作为触发器。

检测彩色主体

OpenCV 提供了一套用于测量和比较图像中颜色分布的函数。这个领域被称为直方图分析。直方图只是各种颜色或颜色范围的像素计数的数组。因此,对于每个通道有 256 个可能值的 BGR 图像,直方图可以有高达 256³ = 1680 万个元素。要创建此类直方图,我们可以使用以下代码:

images = [myImage]  # One or more input images
channels =  [0, 1, 2]  # The channel indices
mask = None  # The image region, or None for everything
histSize = [256, 256, 256]  # The channel depths
ranges = [0, 255, 0, 255, 0, 255]  # The color bin boundaries
hist = cv2.calcHist(images, channels, mask, histSize, ranges)

直方图值的总和等于输入图像中的像素总数。为了便于比较,我们应该将直方图归一化,使其值的总和为 1.0,换句话说,每个值代表属于给定颜色分组的像素的比例。我们可以使用以下代码执行此类归一化:

normalizedHist = cv2.normalize(hist, norm_type=cv2.NORM_L1)

然后,为了获得两个归一化直方图的相似度测量值,我们可以使用如下代码:

method = cv2.HISTCMP_INTERSECT  # A method of comparison
similarity = cv2.compareHist(
  normalizedHist, otherNormalizedHist, method)

对于HISTCMP_INTERSECT方法,相似度是两个直方图的每个元素的最小值的总和。如果我们把直方图看作两条曲线,这个值衡量的是曲线下方的交叠面积。

注意

要查看所有支持的直方图比较方法及其数学定义的列表,请参阅官方文档中的docs.opencv.org/3.0-beta/modules/imgproc/doc/histograms.html#comparehist

我们将构建一个使用直方图相似性作为触发器的相机陷阱。当网络摄像头的图像直方图与参考图像的直方图足够相似时,我们将激活照相机。参考图像可以是彩色风景(如果我们对风景的所有颜色都感兴趣),或者它可以是彩色物体的紧密裁剪照片(如果我们只对物体的颜色感兴趣,而不管周围环境如何)。考虑以下紧密裁剪照片的例子:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00027.jpeg

第一张图像(左)展示了一件橙色夹克,这是狩猎季节期间常见的户外服装。 (鲜艳、温暖的色彩使穿着者更易被看到,从而降低了狩猎事故的风险。) 如果我们想检测树林中的人,这可能是一个好的参考图像。第二张图像(右)展示了一种高山罂粟,其花瓣为红色,花蕊为黄色。如果我们想检测花朵开放时的情况,这可能是一个好的参考图像。

注意

这些以及其他丰富多彩的图像可以在本书的 GitHub 仓库中找到,网址为github.com/OpenCVBlueprints/OpenCVBlueprints/tree/master/chapter_2/CameraTrap/media

让我们在名为set_color_trap.py的新脚本中实现基于颜色的相机陷阱。大部分代码将与set_motion_trap.py相似,但我们将在这里介绍差异。

在某些情况下,set_color_trap.py将打印错误信息到stderr。为了实现这一功能,Python 2 和 Python 3 有不同的语法。我们将添加以下导入语句以实现兼容性,即使我们在运行 Python 2,也能使 Python 3 的print语法可用:

from __future__ import print_function

我们的脚本命令行参数将包括参考图像的路径和一个相似度阈值,这将决定陷阱的灵敏度。以下是参数的定义:

def main():

  parser = argparse.ArgumentParser(
    description='This script detects colors using an '
                'attached webcam. When it detects colors '
                'that match the histogram of a reference '
                'image, it captures photos on an '
                'attached gPhoto2-compatible photo '
                'camera.')

  # ...

  parser.add_argument(
    '--reference-image', type=str, required=True,
    help='path to reference image, whose colors will be '
         'detected in scene')
  parser.add_argument(
    '--min-similarity', type=float, default=0.02,
    help='similarity score that histogram comparator '
         'must find in order to trigger similarity event '
         '(default=0.02, valid_range=[0.0, 1.0])')

  # ...

注意

要阅读此脚本中省略的部分,请访问本书的 GitHub 仓库,网址为github.com/OpenCVBlueprints/OpenCVBlueprints/chapter_2/CameraTrap/set_color_trap.py

我们将解析参数并尝试从文件中加载参考图像。如果无法加载图像,脚本将打印错误信息并提前退出,如下面的代码所示:

  args = parser.parse_args()

  # ...

  reference_image = cv2.imread(args.reference_image,
                               cv2.IMREAD_COLOR)
  if reference_image is None:
    print('Failed to read reference image: %s' %
          args.reference_image, file=sys.stderr)
    return

  min_similarity = args.min_similarity

  # ...

我们将创建参考图像的归一化直方图,稍后,我们还将创建来自网络摄像头的每一帧的归一化直方图。为了帮助创建归一化直方图,我们将在本地定义另一个函数。(Python 允许嵌套函数定义。)以下是相关代码:

  # ...

  channels = range(3)
  hist_size = [256] * 3
  ranges = [0, 255] * 3

  def create_normalized_hist(image, hist=None):
    hist = cv2.calcHist(
      [image], channels, None, hist_size, ranges, hist)
    return cv2.normalize(hist, hist, norm_type=cv2.NORM_L1)

  reference_hist = create_normalized_hist(reference_image)
  query_hist = None

  # ...

再次强调,每次我们从网络摄像头捕获一帧时,我们将找到其归一化直方图。然后,我们将根据比较的HISTCMP_INTERSECT方法测量参考直方图和当前场景直方图的相似度,这意味着我们只想计算直方图的交集或重叠区域。如果相似度等于或大于阈值,我们将开始捕获照片。

这里是主循环的实现:

  while True:
    time.sleep(detection_interval)
    success, bgr = cap.read(bgr)
    if success:
      query_hist = create_normalized_hist(
        bgr, query_hist)
      similarity = cv2.compareHist(
        reference_hist, query_hist, cv2.HISTCMP_INTERSECT)
      if debug:
        print('similarity=%f' % similarity)
      if similarity >= min_similarity and not cc.capturing:
        if photo_ev_step is not None:
          cc.capture_exposure_bracket(photo_ev_step, photo_count)
        else:
          cc.capture_time_lapse(photo_interval, photo_count)

这就结束了main()函数。再次强调,为了确保在脚本执行时调用main(),我们将添加以下代码:

if __name__ == '__main__':
  main()

使脚本可执行。然后,例如,我们可以这样运行它:

$ ./set_color_trap.py --reference-image media/OrangeCoat.jpg --min-similarity 0.13 --width 640 --height 480 --debug True

请看以下图像对。左侧图像显示了相机陷阱的物理设置,正在运行set_color_trap.py,并使用我们刚才提到的自定义参数。右侧图像是结果照片之一:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00028.jpeg

再次强调,这些图像来自不同的相机,它们以不同的方式呈现场景的颜色和对比度。

你可能想尝试set_color_trap的参数,特别是参考图像和相似度阈值。请注意,比较方法HISTCMP_INTERSECT倾向于产生较低的相似度,因此默认阈值仅为 0.02,即直方图的重叠为 2%。如果你修改代码以使用不同的比较方法,你可能需要一个更高的阈值,并且最大相似度可能超过 1.0。

一旦你完成基于颜色的相机陷阱测试,我们就继续使用人脸检测作为我们的最终触发方式。

检测哺乳动物的脸

如你所知,OpenCV 的CascadeClassifier类对于人脸检测和其他类型的对象检测非常有用,它使用一个称为级联的对象特征模型,该模型从 XML 文件中加载。我们在第一章的Supercharging the GS3-U3-23S6M-C and other Point Grey Research cameras部分使用了CascadeClassifierhaarcascade_frontalface_alt.xml进行人脸检测,充分利用您的相机系统。在本书的后续章节中,在第五章的Generic Object Detection for Industrial Applications中,我们将检查CascadeClassifier的所有功能,以及一组用于创建任何类型对象的级联的工具。目前,我们将继续使用 OpenCV 附带的前训练级联。值得注意的是,OpenCV 为人类和猫的人脸检测提供了以下级联文件:

  • 对于人类的前脸:

    • data/haarcascades/haarcascade_frontalface_default.xml

    • data/haarcascades/haarcascade_frontalface_alt.xml

    • data/haarcascades/haarcascade_frontalface_alt2.xml

    • data/lbpcascades/lbpcascade_frontalface.xml

  • 对于人类的侧面脸:

    • data/haarcascades/haarcascade_profileface.xml

    • data/lbpcascades/lbpcascade_profileface.xml

  • 对于猫的前脸:

    • data/haarcascades/haarcascade_frontalcatface.xml

    • data/haarcascades/haarcascade_frontalcatface_extended.xml

    • data/lbpcascades/lbpcascade_frontalcatface.xml

LBP 级联比 Haar 级联更快,但稍微不太准确。Haar 级联的扩展版本(如haarcascade_frontalcatface_extended.xml中使用的那样)对水平和对角特征都很敏感,而标准的 Haar 级联只对水平特征敏感。例如,猫的胡须可能会被识别为对角特征。

注意

本书第五章通用工业应用对象检测将详细讨论级联类型。此外,有关如何使用 OpenCV 的猫级联进行训练的完整教程,请参阅 Joseph Howse(Packt Publishing,2015 年)所著的《OpenCV for Secret Agents》一书中的第三章使用机器学习识别面部表情。

顺便说一下,猫脸检测级联也可能检测到其他哺乳动物的面部。以下图片是使用haarcascade_frontalcatface_extended.xml在猫(左)、红熊猫(右上)和猞猁(右下)的照片上检测结果的可视化:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00029.jpeg

注意

红熊猫和猞猁的照片由 Mathias Appel 拍摄,他慷慨地将这些以及其他许多图片发布到公共领域。请参阅他的 Flickr 页面www.flickr.com/photos/mathiasappel/

让我们在名为set_classifier_trap.py的新脚本中实现基于分类的相机陷阱。必要的导入与set_color_trap.py相同。set_classifier_trap.py的命令行参数包括级联文件的路径以及影响CascadeClassifier使用的其他参数。以下是相关代码:

def main():

  parser = argparse.ArgumentParser(
    description='This script detects objects using an '
                'attached webcam. When it detects '
                'objects that match a given cascade '
                'file, it captures photos on an attached '
                'gPhoto2-compatible photo camera.')

  # ...

  parser.add_argument(
    '--cascade-file', type=str, required=True,
    help='path to cascade file that classifier will use '
         'to detect objects in scene')
  parser.add_argument(
    '--scale-factor', type=float, default=1.05,
    help='relative difference in scale between '
         'iterations of multi-scale classification '
         '(default=1.05)')
  parser.add_argument(
    '--min-neighbors', type=int, default=8,
    help='minimum number of overlapping objects that '
         'classifier must detect in order to trigger '
         'classification event (default=8)')
  parser.add_argument(
    '--min-object-width', type=int, default=40,
    help='minimum width of each detected object'
         '(default=40)')
  parser.add_argument(
    '--min-object-height', type=int, default=40,
    help='minimum height of each detected object'
         '(default=40)')

  # ...

注意

要阅读此脚本省略的部分,请访问本书的 GitHub 仓库github.com/OpenCVBlueprints/OpenCVBlueprints/chapter_2/CameraTrap/set_classifier_trap.py

在像往常一样解析参数之后,我们将使用指定的级联文件初始化一个CascadeClassifier实例。如果文件加载失败,我们将打印错误消息并提前退出脚本。请参阅以下代码:

  args = parser.parse_args()

  # ...

  classifier = cv2.CascadeClassifier(args.cascade_file)
  if classifier.empty():
    print('Failed to read cascade file: %s' %
          args.cascade_file, file=sys.stderr)
    return

  scale_factor = args.scale_factor
  min_neighbors = args.min_neighbors
  min_size = (args.min_object_width, args.min_object_height)

  # ...

在脚本主循环的每次迭代中,我们将网络摄像头图像转换为均衡的黑白版本,并将其传递给CascadeClassifierdetectMultiScale方法。我们将使用一些命令行参数作为额外的参数来控制detectMultiScale的灵敏度。如果至少检测到一个面部(或其他相关对象),我们将开始捕获照片,就像往常一样。以下是循环的实现:

  while True:
    time.sleep(detection_interval)
    success, bgr = cap.read(bgr)
    if success:
      gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY, gray)
      gray = cv2.equalizeHist(gray, gray)
      objects = classifier.detectMultiScale(
        gray, scaleFactor=scale_factor,
        minNeighbors=min_neighbors, minSize=min_size)
      num_objects = len(objects)
      if debug:
        print('num_objects=%d' % num_objects)
      if num_objects > 0 and not cc.capturing:
        if photo_ev_step is not None:
          cc.capture_exposure_bracket(photo_ev_step, photo_count)
        else:
          cc.capture_time_lapse(photo_interval, photo_count)

这完成了main()函数,剩下的只是在脚本执行时像往常一样调用main()

if __name__ == '__main__':
  main()

使脚本可执行。然后,例如,我们可以这样运行它:

$ ./set_classifier_trap.py --cascade-file cascades/haarcascade_frontalcatface_extended.xml --min-neighbors 16 --scale-factor 1.2 --width 640 --height 480 --debug True

参考以下一组图片。左侧图片展示了相机陷阱的物理设置,它正在运行set_classifier_trap.py,并使用我们刚刚提到的自定义参数。右侧图片是其中两张结果照片:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00030.jpeg

左侧图像和右侧图像来自两个不同的相机,因此颜色和对比度不同。此外,两个右侧图像来自set_classifier_trap.py的单独运行,照明条件和相机位置略有变化。

随意尝试set_classifier_trap.py的参数。你可能甚至想创建自己的级联文件来检测不同类型的面部或对象。第五章,工业应用中的通用对象检测,将提供大量信息,帮助你更好地使用CascadeClassifier和级联文件。

接下来,我们将考虑处理我们可能用任何脚本或简单的 gPhoto2 命令捕获的相片的方法。

处理图像以显示细微的颜色和运动

到目前为止,你可能已经捕获了一些曝光包围的相片和延时摄影相片。使用照片管理应用、文件浏览器或以下 gPhoto2 命令将它们上传到你的电脑上:

$ gphoto2 --get-all-files

后者命令会将文件上传到当前工作目录。

我们将合并曝光包围的相片来创建 HDR 图像,这将改善阴影和亮部的色彩表现。同样,我们将合并延时摄影相片来创建延时视频,这将展示加速尺度上的渐进运动。我们将首先处理来自书籍 GitHub 仓库的一些样本相片,该仓库地址为github.com/OpenCVBlueprints/OpenCVBlueprints/tree/master/chapter_2/CameraTrap/media,然后你将能够将代码适配以使用你的相片。

创建 HDR 图像

OpenCV 3 有一个名为“photo”的新模块。其中两个类,MergeDebevecMergeMertens,通过合并曝光包围的相片来创建 HDR 图像。无论使用哪个类,生成的 HDR 图像的通道值都在范围[0.0, 1.0]内。MergeDebevec生成的 HDR 图像在可以显示或打印之前需要伽玛校正。该照片模块提供了几个色调映射函数,能够执行校正。

另一方面,MergeMertens生成的 HDR 图像不需要伽玛校正。其通道值只需放大到范围[0, 255]。我们将使用MergeMertens,因为它更简单,并且通常在保留颜色饱和度方面表现更好。

注意

有关 OpenCV 3 中 HDR 成像和色调映射的更多信息,请参阅官方文档docs.opencv.org/3.0-beta/modules/photo/doc/hdr_imaging.html。还可以查看官方教程docs.opencv.org/3.0-beta/doc/tutorials/photo/hdr_imaging/hdr_imaging.html

MergeDebevecMergeMertens类分别基于以下论文:

P. Debevec, and J. Malik, 从照片中恢复高动态范围辐射图, ACM SIGGRAPH 会议论文集,1997 年,369 - 378。

T. Mertens, J. Kautz, and F. Van Reeth, 曝光融合, 第 15 届太平洋计算机图形和应用会议论文集,2007 年,382 - 390。

为了演示目的,GitHub 仓库包含一对名为 Plasma 的猫的曝光包围照片。(她的照片和 HDR 合并版本在本章的规划相机陷阱部分中较早出现。)让我们创建一个脚本,test_hdr_merge.py,以合并未处理的照片,media/PlasmaWink_0.jpgmedia/PlasmaWink_1.jpg。以下是实现方式:

#!/usr/bin/env python

import cv2

def main():

  ldr_images = [
    cv2.imread('media/PlasmaWink_0.jpg'),
    cv2.imread('media/PlasmaWink_1.jpg')]

  hdr_processor = cv2.createMergeMertens()
  hdr_image = hdr_processor.process(ldr_images) * 255
  cv2.imwrite('media/PlasmaWink_HDR.jpg', hdr_image)

if __name__ == '__main__':
  main()

从仓库中获取脚本和媒体文件,运行脚本,查看生成的 HDR 图像。然后,修改脚本以处理您自己的曝光包围照片。HDR 可以为任何具有强烈光线和深阴影的场景产生戏剧性的效果。风景和阳光照射的房间是很好的例子。

使用 HDR 成像,我们压缩了曝光差异。接下来,通过延时摄影,我们将压缩时间差异。

创建延时视频

在第一章的超级充电 PlayStation Eye部分中,我们创建了一个慢动作视频。记住,我们只是以高速(187 FPS)捕获图像,并将它们放入一个配置为以正常速度(60 FPS)播放的视频中。同样,要创建延时视频,我们将读取以低速(小于 1 FPS)捕获的图像文件,并将它们放入一个配置为以正常速度(60 FPS)播放的视频中。

为了演示目的,本书的 GitHub 仓库包含一组名为 Josephine 的猫的延时照片。当我们为 Josephine 制作延时视频时,我们会看到她非常活跃,即使她坐在椅子上时也是如此!作为预览,以下是延时视频的三个连续帧:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/ocv3-bp/img/00031.jpeg

该系列包含 56 张照片,名称从media/JosephineChair_00.jpgmedia/JosephineChair_55.jpg。以下脚本,我们将称之为test_time_lapse_merge.py,将读取照片并生成一个名为media/JosephineChair_TimeLapse.avi的一秒延时视频:

#!/usr/bin/env python

import cv2

def main():

  num_input_files = 56
  input_filename_pattern = 'media/JosephineChair_%02d.jpg'
  output_filename = 'media/JosephineChair_TimeLapse.avi'
  fourcc = cv2.VideoWriter_fourcc('M', 'J', 'P', 'G')
  fps = 60.0
  writer = None

  for i in range(num_input_files):
    input_filename = input_filename_pattern % i
    image = cv2.imread(input_filename)
    if writer is None:
      is_color = (len(image.shape) > 2)
      h, w = image.shape[:2]
      writer = cv2.VideoWriter(
        output_filename, fourcc, fps, (w, h), is_color)
    writer.write(image)

if __name__ == '__main__':
    main()

从仓库中获取脚本和媒体,运行脚本,观看约瑟芬从她的椅子上观看世界的视频结果。然后,修改脚本以处理你自己的图像。也许你会捕捉到其他慢动作动物的运动,花朵的绽放,或者阳光和云朵穿越景观的景象。

作为进一步的项目,你可能希望创建 HDR 延时视频。你可以通过修改我们的capture_exposure_bracket.sh脚本来开始,捕捉多批曝光包围的图像,每批之间有时间延迟。(例如,可以使用sleep 3命令延迟 3 秒。)将捕获的图像上传到你的电脑后,你可以将每批合并成 HDR 图像,然后将 HDR 图像合并成延时视频。

探索其他摄影技术,然后尝试自动化它们!

进一步学习

计算摄影是一个多样化和受欢迎的领域,它结合了艺术家、技术人员和科学家的工作。因此,有许多类型的作者、讲师和导师可以帮助你成为一名更好的“计算摄影师”。以下是一些有用的指南示例:

  • 《使用 OpenCV 学习图像处理》,作者格洛丽亚·布埃诺·加西亚等(Packt Publishing,2015 年),涵盖了 OpenCV 3 在图像捕捉、图像编辑和计算摄影方面的广泛功能。本书使用 C++编写,适合计算机视觉初学者。

  • 《国家地理摄影大师视频讲座》(The Great Courses,2015 年)提供了对大师摄影师目标和技术的深刻见解。几位讲师是野生动物摄影师,他们使用相机陷阱的做法为这一章节提供了灵感。

  • 《开源天体摄影》,作者卡尔·萨诺(CreateSpace Independent Publishing Platform,2013 年),涵盖了使用 gPhoto2 和其他开源软件,以及摄影硬件来捕捉和处理夜空详细图像的使用方法。

  • 《好奇摄影师的科学》,作者查尔斯·S·约翰逊(CRC Press,2010 年),解释了光、镜头和摄影的科学历史和原理。此外,它还提供了解决常见摄影问题的实用解决方案,例如为微距摄影选择和设置良好的设备。

不论是作为爱好还是职业,计算摄影都是一种探索和记录世界的绝佳方式,从特定的视角来看。它需要观察、实验和耐心,所以请放慢脚步!花时间从他人的探索中学习,并分享你的经验。

摘要

本章展示了一套令人惊讶的灵活的命令和类,使我们能够用简短和简单的代码进行计算摄影实验。我们已经编写了控制相机的脚本。在这个过程中,我们熟悉了 gPhoto2、Bash shell、PTP 通信、GVFS 挂载点和 Python 对子进程的支持。我们还编写了几个照片陷阱的变体,以便在主题进入视野时拍照。为此,OpenCV 为我们提供了检测运动、测量颜色相似性和分类对象的能力。最后,我们使用 OpenCV 将一组照片组合成时间流逝视频或 HDR 图像。

到目前为止,这本书已经提供了一种相当广泛的概述,介绍了如何捕捉光作为数据、控制相机、检测主题以及处理照片的方法。接下来的章节将专注于一系列高级技术,这将使我们能够对图像的主题进行更精细的分类和识别,并且能够以考虑相机运动和视角的方式处理照片和视频。

Logo

更多推荐