OpenCV 实践指南(一)

原文:Practical OpenCV

协议:CC BY-NC-SA 4.0

一、计算机视觉和 OpenCV 简介

Abstract

当我们醒着的时候,我们从这个世界获得的信息中有很大一部分是通过视觉获得的。我们的眼睛做了一件奇妙的工作,不停地旋转,根据需要改变焦点来看东西。我们的大脑在处理来自双眼的信息流、创建我们周围世界的 3D 地图以及让我们意识到自己在这张地图中的位置和方向方面做得更好。如果机器人(以及一般的计算机)能像我们一样看到并理解他们看到的东西,那不是很酷吗?

当我们醒着的时候,我们从这个世界获得的信息中有很大一部分是通过视觉获得的。我们的眼睛做了一件奇妙的工作,不停地旋转,根据需要改变焦点来看东西。我们的大脑在处理来自双眼的信息流、创建我们周围世界的 3D 地图以及让我们意识到自己在这张地图中的位置和方向方面做得更好。如果机器人(以及一般的计算机)能像我们一样看到并理解他们看到的东西,那不是很酷吗?

对于机器人来说,视觉本身不是什么问题——各种各样的相机都有,而且非常容易使用。然而,对于一台连接有摄像头的计算机来说,摄像头馈送从技术上来说只是一组随时间变化的数字。

进入计算机视觉。

计算机视觉就是让机器人足够聪明,能够根据它们看到的东西做出决定。

为什么写这本书?

在我看来,今天的机器人就像 35 年前的个人电脑——一项新兴技术,有可能彻底改变我们的日常生活方式。如果有人带你提前 35 年,不要惊讶地看到机器人在街上漫步,在建筑物内工作,在许多日常任务中帮助人类并与人类安全合作。如果你在工业和医院里看到机器人,轻松地执行最复杂和要求最精确的任务,也不要感到惊讶。你猜对了,要做到这一切,他们需要高效、智能、鲁棒的视觉系统。

计算机视觉可能是当今机器人领域最热门的研究领域。世界各地有很多聪明人试图设计算法并实现它们,让机器人有能力智能和正确地解释他们看到的东西。如果你也想在这个研究领域有所贡献,这本书是你的第一步。

在这本书里,我打算通过一系列越来越复杂的项目,在计算机视觉研究的一些最重要的领域里,教你一些基本的概念,以及一些稍微高级的概念。从让计算机识别颜色这样简单的事情开始,我将带领你经历一段旅程,甚至教你如何让机器人根据其摄像头馈送中的对象如何移动来估计其速度和方向。

我们将在一个名为 OpenCV 的编程库(粗略地说,是一组可以执行相关高级任务的预写函数)的帮助下实现我们所有的项目。

这本书将让您熟悉 OpenCV 通过其内置函数提供的算法实现、算法的理论细节以及使用 OpenCV 时通常采用的 C++编程原理。在本书的结尾,我们还将讨论几个项目,在这些项目中,我们将 OpenCV 的框架用于我们自己设计的算法。将假设对 C++编程的熟悉程度适中。

开放计算机视觉

开源计算机视觉。org 是计算机视觉的瑞士军刀。它有各种各样的模块,可以帮助你解决很多计算机视觉问题。但是 OpenCV 最有用的部分可能是它的架构和内存管理。它为您提供了一个框架,您可以使用 OpenCV 的算法或您自己的算法,以任何方式处理图像和视频,而不必担心为图像分配和取消分配内存。

OpenCV 的历史

探究一下 OpenCV 为什么以及如何被创建是很有趣的。OpenCV 作为英特尔研究院的一个研究项目正式启动,旨在推进 CPU 密集型应用中的技术。该项目的许多主要贡献者包括英特尔俄罗斯研究院和英特尔性能库团队的成员。该项目的目标如下:

  • 通过为基础视觉基础设施提供开放且优化的代码,推进视觉研究。(不再多此一举!)
  • 通过提供一个开发人员可以构建的公共基础设施来传播视觉知识,这样代码将更易于阅读和转移。
  • 通过免费提供可移植的、性能优化的代码,推进基于视觉的商业应用——许可证不要求应用本身开放或免费。

OpenCV 的第一个 alpha 版本在 2000 年的 IEEE 计算机视觉和模式识别会议上向公众发布。目前,OpenCV 由一个名为OpenCV.org的非营利基金会所有。

内置模块

OpenCV 的内置模块功能强大,用途广泛,足以解决大多数计算机视觉问题,这些问题都有成熟的解决方案。您可以裁剪图像,通过修改亮度、锐度和对比度来增强图像,检测图像中的形状,将图像分割成直观明显的区域,检测视频中的移动物体,识别已知物体,根据摄像头馈送估计机器人的运动,以及使用立体摄像头获得世界的 3D 视图,这只是其中的几个应用。然而,如果你是一名研究人员,想要开发自己的计算机视觉算法,而这些模块本身并不完全足够,OpenCV 仍然会通过其架构、内存管理环境和 GPU 支持来帮助你。你会发现你自己的算法与 OpenCV 高度优化的模块协同工作确实是一个强有力的组合。

OpenCV 模块需要强调的一个方面是它们是高度优化的。它们旨在用于实时应用,旨在跨各种计算平台(从 MacBooks 到运行精简版 Linux 的小型嵌入式 fitPCs)快速执行。

OpenCV 为您提供了一组模块,可以大致执行表 1-1 中列出的功能。

表 1-1。

Built-in modules offered by OpenCV

| 组件 | 功能 | | --- | --- | | 核心 | 核心数据结构、数据类型和内存管理 | | 伊姆普洛克 | 图像过滤、几何图像变换、结构和形状分析 | | 海贵 | GUI,读取和写入图像和视频 | | 录像 | 视频中的运动分析和目标跟踪 | | calib | 摄像机标定和多视图三维重建 | | 功能 2d | 特征提取、描述和匹配 | | object detect(对象检测) | 使用级联和梯度直方图分类器的目标检测 | | 机器语言(Machine Language) | 用于计算机视觉应用的统计模型和分类算法 | | 弗兰恩 | 近似最近邻的快速库—在高维(特征)空间中的快速搜索 | | 国家政治保卫局。参见 OGPU | 并行化选定算法,以便在 GPU 上快速执行 | | 缝 | 用于图像拼接的扭曲、混合和束调整 | | 非免费 | 在某些国家获得专利的算法实现 |

在这本书里,我将介绍利用这些模块的项目。

摘要

我希望这一介绍性章节已经让你对这本书的内容有了一个大概的了解!我心目中的读者群包括对使用 C++知识编写快速计算机视觉应用感兴趣的学生,以及对学习许多最著名算法背后的基本理论感兴趣的学生。如果你已经知道这个理论,并且对学习 OpenCV 语法和编程方法感兴趣,这本书及其大量的代码示例也会对你有用。

下一章讨论在你的计算机上安装和设置 OpenCV,这样你就可以快速开始一些令人兴奋的项目了!

二、在计算机上设置 OpenCV

Abstract

现在你知道了计算机视觉对你的机器人有多重要,以及 OpenCV 如何帮助你实现很多,本章将指导你在你的计算机上安装 OpenCV 和设置开发工作站的过程。这也将允许你尝试和使用本书后续章节中描述的所有项目。官方的 OpenCV 安装 wiki 可以在 http://opencv.willowgarage.com/wiki/InstallGuide 获得,本章将主要在此基础上构建。

现在你知道了计算机视觉对你的机器人有多重要,以及 OpenCV 如何帮助你实现很多,本章将指导你在你的计算机上安装 OpenCV 和设置开发工作站的过程。这也将允许你尝试和使用本书后续章节中描述的所有项目。官方的 OpenCV 安装 wiki 可以在 http://opencv.willowgarage.com/wiki/InstallGuide 获得,本章将主要在此基础上构建。

操作系统

OpenCV 是一个独立于平台的库,因为它可以安装在几乎所有满足特定要求的操作系统和硬件配置上。然而,如果你有选择你的操作系统的自由,我会建议一个 Linux 版本,最好是 Ubuntu(最新的 LTS 版本是 12.04)。这是因为它是免费的,与 Windows 和 Mac OS X 一样好用(有时甚至更好),你可以将许多其他很酷的库与你的 OpenCV 项目集成在一起,如果你计划在嵌入式系统上工作,如 Beagleboard 或 Raspberry Pi,它将是你唯一的选择。

在这一章中,我将提供 Ubuntu、Windows 和 Mac OSX 的安装说明,但主要集中在 Ubuntu 上。后面章节中的项目本身是独立于平台的。

人的本质

http://sourceforge.net/projects/opencvlibrary/ 下载 OpenCV tarball 并解压到一个首选位置(后续步骤我称之为OPENCV_DIR)。您可以通过使用归档管理器或发出 tar–xvf 命令来提取,如果您对它感到满意的话。

简单安装

这意味着您将安装当前稳定的 OpenCV 版本,带有默认编译标志,并且只支持标准库。

If you don’t have the standard build tools, get them by

sudo apt-get install build-essential checkinstall cmake

Make a build directory in OPENCV_DIR and navigate to it by

mkdir build

cd build

Configure the OpenCV installation by

cmake ..

Compile the source code by

make

Finally, put the library files and header files in standard paths by

sudo make install

自定义安装(32 位)

这意味着您将安装许多支持库并配置 OpenCV 安装以考虑它们。我们将安装的额外库包括:

  • FFmpeg、gstreamer、x264 和 v4l,支持视频观看、录制、流式传输等
  • 如果您没有标准的构建工具,请使用

sudo apt-get install build-essential checkinstall cmake

Install gstreamer

sudo apt-get install libgstreamer0.10-0 libgstreamer0.10-dev gstreamer0.10-tools gstreamer0.10-plugins-base libgstreamer-plugins-base0.10-dev gstreamer0.10-plugins-good gstreamer0.10-plugins-ugly gstreamer0.10-plugins-bad gstreamer0.10-ffmpeg

Remove any installed versions of ffmpeg and x264

sudo apt-get remove ffmpeg x264 libx264-dev

Install dependencies for ffmpeg and x264

sudo apt-get update

sudo apt-get install git libfaac-dev libjack-jackd2-dev libmp3lame-dev libopencore-amrnb-dev libopencore-amrwb-dev libsdl1.2-dev libtheora-dev libva-dev libvdpau-dev libvorbis-dev libx11-dev libxfixes-dev libxvidcore-dev texi2html yasm zlib1g-dev libjpeg8 libjpeg8-dev

Get a recent stable snapshot of x264 from ftp://ftp.videolan.org/pub/videolan/x264/snapshots/ , extract it to a folder on your computer and navigate into it. Then configure, build, and install by

./configure –-enable-static

make

sudo make install

Get a recent stable snapshot of ffmpeg from http://ffmpeg.org/download.html , extract it to a folder on your computer and navigate into it. Then configure, build, and install by

./configure --enable-gpl --enable-libfaac --enable-libmp3lame –-enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libtheora --enable-libvorbis –-enable-libx264 --enable-libxvid --enable-nonfree --enable-postproc --enable-version3 –-enable-x11grab

make

sudo make install

Get a recent stable snapshot of v4l from http://www.linuxtv.org/downloads/v4l-utils/ , extract it to a folder on your computer and navigate into it. Then build and install by

make

sudo make install

Install cmake-curses-gui, a semi-graphical interface to CMake that will allow you to see and edit installation flags easily

sudo apt-get install cmake-curses-gui

Make a build directory in OPENCV_DIR by

mkdir build

cd build

Configure the OpenCV installation by

ccmake ..

Press ‘c’ to configure and ‘g’ to generate, and then build and install by

表 2-1。

Configuration flags for installing OpenCV with support for other common libraries

| 旗 | 价值 | | --- | --- | | 构建 _ 文档 | 安大略 | | 构建示例 | 安大略 | | 安装示例 | 安大略 | | WITH_GSTREAMER | 安大略 | | 使用 _JPEG | 安大略 | | 带 _PNG | 安大略 | | WITH_QT | 安大略 | | WITH_FFMPEG | 安大略 | | 带 _V4L | 安大略 |

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-1。

Configuration flags when you start installing OpenCV Press ‘c’ to start configuring. CMake-GUI should do its thing, discovering all the libraries you installed above, and present you with a screen showing the installation flags (Figure 2-1).   You can navigate among the flags by the up and down arrows, and change the value of a flag by pressing the Return key. Change the following flags to the values shown in Table 2-1.

make

sudo make install

Tell Ubuntu where to find the OpenCV shared libraries by editing the file opencv.conf (first time users might not have that file—in that case, create it)

sudo gedit /etc/ld.so.conf.d/opencv.conf

Add the line ‘/usr/local/lib’ (without quotes) to this file, save and close. Bring these changes into effect by

sudo ldconfig /etc/ld.so.conf

Similarly, edit /etc/bash.bashrc and add the following lines to the bottom of the file, save, and close:

PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig

export PKG_CONFIG_PATH

重新启动计算机。

自定义安装(64 位)

如果你用的是 64 位版本的 Ubuntu,除了以下变化,这个过程基本上是一样的。

During the step 5 to configure x264, use this command instead:

./configure --enable-shared –-enable-pic

During the step 6 to configure ffmpeg, use this command instead:

./configure --enable-gpl --enable-libfaac --enable-libmp3lame –-enable-libopencore-amrnb –-enable-libopencore-amrwb --enable-libtheora --enable-libvorbis --enable-libx264 --enable-libxvid --enable-nonfree --enable-postproc --enable-version3 --enable-x11grab –-enable-shared –-enable-pic

检查安装

您可以通过将以下代码放入名为 hello_opencv.cpp 的文件中来检查安装。它会显示一个图像,并在您按下“q”时关闭窗口:

#include <iostream>

#include <opencv2/highgui/highgui.hpp>

using namespace std;

using namespace cv;

int main(int argc, char **argv)

{

Mat im = imread("image.jpg", CV_LOAD_IMAGE_COLOR);

namedWindow("Hello");

imshow("Hello", im);

cout << "Press 'q' to quit..." << endl;

while(1)

{

if(char(waitKey(1)) == 'q') break;

}

destroyAllWindows();

return 0;

}

Open up that directory in a Terminal and give the following command to compile the code:

g++ 'pkg-config opencv --cflags' hello_opencv.cpp -o hello_opencv 'pkg-config opencv --libs'

Run the compiled code by

./hello_opencv

注意,为了运行这个程序,在同一个目录中需要有一个名为“image.jpg”的图像。

没有超级用户权限的情况下安装

很多时候,您对正在使用的计算机没有超级用户访问权限。如果你告诉 Ubuntu 在哪里寻找库和头文件,你仍然可以安装和使用 OpenCV。事实上,这种使用 OpenCV 的方法比以前的方法更值得推荐,因为根据官方 OpenCV 安装 Wiki 页面,它不会用冲突版本的 OpenCV 文件“污染”系统目录。注意,安装额外的库,比如 Qt、Ffmpeg 等等,仍然需要超级用户权限。但是 OpenCV 在没有这些附件的情况下仍然可以工作。涉及的步骤有:

Download the OpenCV tarball and extract it to a directory where you have read/write rights. We shall call this directory OPENCV_DIR. Make the following directories in OPENCV_DIR

mkdir build

cd build

mkdir install-files

Configure your install as mentioned previously. Change the values of flags depending on which extra libraries you have installed in the system. Also, set the value of CMAKE_INSTALL_PREFIX to OPENCV_DIR/build/install-files.   Continue the same making process as the normal install, up to step 12. Then, run make install instead of sudo make install. This will put all the necessary OpenCV files in OPENCV_DIR/build/install-files.   Now, edit the file ∼/.bashrc (your local bashrc file over which you should have read/write access) and add the following lines to the end of the file, then save and close

export INCLUDE_PATH=<path-to-OPENCV_DIR>/build/install-files/include:$INCLUDE_PATH

export LD_LIBRARY_PATH=<path-to-OPENCV_DIR>/build/install-files/lib:$LD_LIBRARY_PATH

export PKG_CONFIG_PATH=<path-to-OPENCV_DIR>/build/install-files/lib/pkgconfig:$PKG_CONFIG_PATH

其中<path-to-OPENCV_DIR>例如可以是/home/user/libraries/opencv/.

Reboot your computer.   You can now compile and use OpenCV code as mentioned previously, like a normal install.

使用集成开发环境

如果您喜欢在 IDE 中工作而不是在终端中工作,那么您必须配置 IDE 项目来找到您的 OpenCV 库文件和头文件。对于广泛使用的 Code::Blocks IDE,在 http://opencv.willowgarage.com/wiki/CodeBlocks 可以找到非常好的指令,对于任何其他 IDE 来说,这些步骤应该都差不多。

Windows 操作系统

Windows 用户的安装说明可以在 http://opencv.willowgarage.com/wiki/InstallGuide 获得,并且运行得很好。与 MS Visual C++集成的说明可在 http://opencv.willowgarage.com/wiki/VisualC++ 获得。

mac os x

Mac OSX 用户可以按照 http://opencv.willowgarage.com/wiki/Mac_OS_X_OpenCV_Port 的指示在自己的电脑上安装 OpenCV。

摘要

所以你可以看到在 Linux 上安装软件比在 Windows 和 Mac OS X 上有趣多了!玩笑归玩笑,浏览整个过程会给初学者提供关于 Linux 内部工作和终端使用的有价值的见解。如果,甚至在按照指示做了之后,你在安装 OpenCV 的时候还有问题,谷歌你的错误。很有可能其他人也遇到过这个问题,并且他们已经在论坛上询问过这个问题。YouTube 上也有许多网站和详细的视频解释 Linux、Windows 和 Mac OS X 的安装过程。

三、CV Bling——OpenCV 内置演示

Abstract

现在你(希望)已经在电脑上安装了 OpenCV,是时候看看 OpenCV 能为你做什么的一些很酷的演示了。运行这些演示也将有助于确认 OpenCV 的正确安装。

现在你(希望)已经在电脑上安装了 OpenCV,是时候看看 OpenCV 能为你做什么的一些很酷的演示了。运行这些演示也将有助于确认 OpenCV 的正确安装。

OpenCV 附带了许多演示。它们以 C、C++和 Python 代码文件的形式存在于OPENCV_DIR内的samples文件夹中(安装时解压 OpenCV 档案的目录;具体参见第二章。如果您在配置您的安装时指定了标志BUILD_EXAMPLESON,那么编译后的可执行文件应该在OPENCV_DIR/build/bin中可用。如果您没有这样做,您可以在打开标志的情况下再次运行您的配置和安装,如第二章中所述。

让我们看看 OpenCV 提供的一些演示。请注意,您可以通过以下方式运行这些演示

./<demo_name> [options]

其中options是程序期望的一组命令行参数,通常是文件名。下面显示的演示已经在 OpenCV 附带的图像上运行,可以在OPENCV_DIR/samples/cpp中找到。

注意,下面提到的所有命令都是在导航到OPENCV_DIR/build/bin后执行的。

凸轮换档

Camshift 是一个简单的对象跟踪算法。它使用指定对象的亮度和颜色直方图在另一个图像中查找该对象的实例。OpenCV 演示首先要求您在相机馈送中的目标对象周围画一个框。它从该框的内容中生成所需的直方图,然后继续使用 camshift 算法来跟踪相机馈送中的对象。导航至OPENCV_DIR/build/bin运行演示,并执行以下操作

./cpp-example-camshiftdemo

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-3。

Camshift object tracking

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-2。

Camshift object tracking

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-1。

Camshift object tracking—specifying the object to be tracked

但是,camshift 总是试图找到对象的实例。如果对象不存在,它显示最近的匹配作为检测(见图 3-4 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-4。

Camshift giving a false positive

立体匹配

stereo_matching演示展示了 OpenCV 的立体块匹配和视差计算能力。它将两幅图像(由左右立体摄像机拍摄)作为输入,并产生一幅视差为灰色编码的图像。我将在本书的后面用整整一章来讨论立体视觉,同时,简短地解释一下视差:当你使用两个相机(左和右)看到一个物体时,它在两个图像中的水平位置会略有不同,右帧中的物体相对于左帧的位置差异称为视差。视差可以给出关于对象深度的概念,即,它离相机的距离,因为视差与距离成反比。在输出图像中,视差较大的像素较亮。(回想一下,较高的视差意味着离摄像机的距离较小。)您可以通过以下方式在著名的筑波图片上运行演示

./cpp-example-stereo_match OPENCV_DIR/samples/cpp/tsukuba_l.png OPENCV_DIR/samples/cpp/tsukuba_r.png

其中OPENCV_DIR是到OPENCV_DIR的路径

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-5。

OpenCV stereo matching

视频中单应性估计

video_homography演示使用快速角点检测器检测图像中的兴趣点,并匹配关键点处评估的简短描述符。它对“参考”帧和任何其他帧这样做,以估计两个图像之间的单应变换。单应只是将点从一个平面转换到另一个平面的矩阵。在这个演示中,您可以从摄像机画面中选择您的参考帧。演示程序在参考帧和当前帧之间的单应变换方向上画线。你可以运行它

./cpp-example-video_homography 0

其中 0 是摄像机的设备 ID。0 通常意味着笔记本电脑的集成网络摄像头。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-7。

Estimated homography shown by lines

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-6。

The reference frame for homography estimation, also showing FAST corners

圆和线检测

OpenCV 中的 houghcircles 和 houghlines 演示使用 Hough 变换分别检测给定图像中的圆和线。我将在第六章中对霍夫变换有更多的说明。现在,只要知道霍夫变换是一个非常有用的工具,它可以让你检测图像中的规则形状。您可以通过以下方式运行演示

./cpp-example-houghcircles OPENCV_DIR/samples/cpp/board.jpg

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-8。

Circle detection using Hough transform and

./cpp-example-houghlines OPENCV_DIR/samples/cpp/pic1.png

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-9。

Line detection using Hough transform

图象分割法

meanshift_segmentation演示实现了图像分割的 meanshift 算法(区分图像的不同“部分”)。它还允许您设置与算法相关的各种阈值。运行它

./cpp-example-meanshift_segmentation OPENCV_DIR/samples/cpp/tsukuba_l.png

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-10。

Image segmentation using the meanshift algorithm

如你所见,图像中的不同区域颜色不同。

包围盒和圆形

演示程序找到包围一组点的最小的矩形和圆形。在演示中,点是从图像区域内随机选择的。

./cpp-example-minarea

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-11。

Bounding box and circle

图像修复

图像修复是用周围的像素替换图像中的某些像素。它主要用于修复图像的损坏,如意外的笔触。OpenCV inpaint演示允许你通过在图像上做白色标记来破坏图像,然后运行修复算法来修复损坏。

./cpp-example-inpaint OPENCV_DIR/samples/cpp/fruits.jpg

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-12。

Image inpainting

摘要

本章的目的是让您对 OpenCV 的各种能力有所了解。还有很多其他的演示。请随意尝试,以获得更好的想法。一个特别著名的 OpenCV 演示是使用哈尔级联的人脸检测。主动的读者也可以浏览这些示例的源代码,可以在OPENCV_DIR/samples/cpp中找到。本书中的许多未来项目将利用这些样本中的代码片段和思想。

四、图像和 GUI 窗口的基本操作

Abstract

在这一章中,你将最终开始接触你自己编写的 OpenCV 代码。我们将从一些简单的任务开始。本章将教你如何:

在这一章中,你将最终开始接触你自己编写的 OpenCV 代码。我们将从一些简单的任务开始。本章将教你如何:

  • 在窗口中显示图像
  • 将您的图像从彩色转换为灰度
  • 创建 GUI 跟踪条并编写回调函数
  • 从图像中裁剪部分
  • 访问图像的单个像素
  • 阅读、显示和编写视频

我们开始吧!从本章开始,我将假设您知道如何编译和运行您的代码,您熟悉目录/路径管理,并且您将把程序需要的所有文件(例如,输入图像)放在与可执行文件相同的目录中。

我还建议你在 http://docs.opencv.org/ 广泛使用 OpenCV 文档。在本书中不可能讨论所有 OpenCV 函数的所有形式和用例。但是在文档页面上,关于所有 OpenCV 函数及其用法语法和参数类型的信息是以一种非常容易理解的方式组织起来的。所以每当你看到本书中介绍的新功能时,养成在文档中查找的习惯。您将熟悉使用该函数的各种方法,并且可能还会遇到几个相关的函数,这将增加您的技能。

在窗口中显示来自磁盘的图像

在 OpenCV 中显示磁盘映像非常简单。highgui模块的imread()namedWindow()imshow()功能为您完成所有工作。看一下清单 4-1,它在一个窗口中显示了一个图像,当你按 Esc 或‘Q’或‘Q’时退出(这和我们在第二章中用来检查 OpenCV 安装的代码完全一样):

清单 4-1。在窗口中显示图像

#include <iostream>

#include <opencv2/highgui/highgui.hpp>

using namespace std;

using namespace cv;

int main(int argc, char **argv)

{

Mat im = imread("image.jpg", CV_LOAD_IMAGE_COLOR);

namedWindow("Hello");

imshow("Hello", im);

cout << "Press 'q' to quit..." << endl;

while(char(waitKey(1)) != 'q') {}

return 0;

}

我现在将把代码分成几个部分来解释。

Mat im = imread("image.jpg", CV_LOAD_IMAGE_COLOR);

这创建了一个类型为cv::Mat的变量im(我们只写了Mat而不是cv::Mat,因为我们已经使用了上面的名称空间cv;,这是标准做法)。它还从磁盘中读取名为image.jpg的图像,并通过函数imread(). CV_LOAD_IMAGE_COLORis flag(在highgui.hpp头文件中定义的常量)将其放入im中,该函数告诉imread()将图像作为彩色图像加载。彩色图像有三个通道——红色、绿色和蓝色,而灰度图像只有一个通道——强度。您可以使用标志CV_LOAD_IMAGE_GRAYSCALE将图像加载为灰度。这里的im类型为CV_8UC3,其中 8 表示每个通道中每个像素所占的位数,U表示无符号字符(每个像素的每个通道是一个 8 位无符号字符),而C3表示 3 个通道。

namedWindow("Hello");

imshow("Hello", im);

首先创建一个名为 Hello 的窗口(Hello 也显示在窗口的标题栏中),然后在窗口中显示存储在im中的图像。就是这样!剩下的代码只是为了防止 OpenCV 在用户按下’ Q ‘或’ Q '之前退出并破坏窗口。

这里一个值得注意的函数是waitKey()。这将无限期等待一个按键事件(当n <= 0为正时)或等待n毫秒。它返回所按下的键的 ASCII 码,或者如果在指定时间过去之前没有按下键,则返回-1。注意waitKey()只有在 OpenCV GUI 窗口打开并处于焦点时才起作用。

cv::Mat 结构

cv::Mat 结构是 OpenCV 中用于存储数据(图像等)的主要数据结构。稍微走点弯路,了解一下 cv::Mat 有多牛逼是值得的。

cv::Mat 被组织为一个标题和实际数据。因为数据的布局与其他库和 SDK 中使用的数据结构相似或兼容,所以这种组织允许非常好的互操作性。可以为用户分配的数据创建一个 cv::Mat 头,并使用 OpenCV 函数就地处理它。

表 4-1 、 4-2 、 4-3 描述了 cv::Mat 结构的一些常见操作。不要担心马上就能记住;相反,通读一遍,了解你能做的事情,然后使用这些表格作为参考。

创建 cv::Mat

表 4-1。

Creating a cv::Mat

| 句法 | 描述 | | --- | --- | | 双 m[2][2] = {{1.0,2.0},{3.0,4.0 } };mat m(2.2,CV_32F,m); | 从多维数组数据创建一个 2 x 2 矩阵 | | Mat M(100,100,CV_32FC2,标量(1,3)); | 创建一个 100 x 100 的双通道矩阵,第一个通道填充 1,第二个通道填充 3 | | M.create(300,300,CV _ 8UC(15)); | 创建一个 300 x 300 的 15 通道矩阵,以前分配的数据将被释放 | | int size[3]= { 7,8,9 };Mat M(3,sizes,CV_8U,Scalar::all(0)); | 创建一个三维数组,其中每个维度的大小分别为 7、8 和 9。该数组用 0 填充 | | mat m = mat::eye(7.7,cv _ 32f); | 创建一个 7 x 7 的单位矩阵,每个元素是一个 32 位的浮点数 | | Mat M = Mat::零(7.7,cv _ 64f); | 创建一个用 64 位浮点零填充的 7 x 7 矩阵。类似地,Mat::ones()创建用 1 填充的矩阵 |

访问 cv::Mat 的元素

表 4-2。

Accessing elements from a cv::Mat

| 句法 | 描述 | | --- | --- | | M.at (i,j) | 访问 M 的第 I 行第 j 列的元素,M 是一个双精度矩阵。请注意,行号和列号从 0 开始 | | M.row(1) | 访问 m 的第一行。注意行数从 0 开始 | | 男 col(3) | 访问 m 的第 3 列。同样,列数从 0 开始 | | m . rowrange(1.4) | 访问 M 的第 1 到第 4 行 | | m . col range(1.4) | 访问 M 的第 1 到第 4 列 | | m . rowrange(2.5)。colrange(1.3) | 访问 M 的第 2 至第 5 行和第 1 至第 3 列 | | M.diag() | 访问方阵的对角线元素。也可用于从一组对角线值创建方阵 |

带 cv::Mat 的表达式

表 4-3。

Expressions with a cv::Mat

| 句法 | 描述 | | --- | --- | | 马特·M2 = m1 . clone(); | 让 M2 成为 M1 的翻版 | | Mat M2(消歧义):m1 . copy to(m2); | 让 M2 成为 M1 的翻版 | | Mat M1 = Mat::零(9.3,cv _ 32 fc 3);mat m2 = m1 . reshape(0.3); | 使 M2 成为具有与 M1 相同数量通道(由 0 表示)和 3 行(因此 9 列)的矩阵 | | mat m2 = m1 . t(); | 使 M2 成为 M1 的翻版 | | mat m2 = m1 . inv(); | 使 M2 成为 M1 的反面 | | mat m3 = m1 * m2; | 使 M3 成为 M1 和 M2 的母体产物 | | 马特·M2 = M1+s; | 将标量 s 添加到矩阵 M1,并将结果存储在 M2 中 |

关于 cv::Mats 的更多操作可以在 OpenCV 文档页面的 http://docs.opencv.org/modules/core/doc/basic_structures.html#mat 找到。

色彩空间之间的转换

色彩空间是描述图像中颜色的一种方式。最简单的颜色空间是 RGB 颜色空间,它只是将每个像素的颜色表示为红色、绿色和蓝色值,因为红色、绿色和蓝色是原色,您可以通过以各种比例组合这三种颜色来创建所有其他颜色。通常,每个“通道”是一个 8 位无符号整数(取值范围为 0-255);因此,您会发现 OpenCV 中的大多数彩色图像都具有 CV_8UC3 类型。表 4-4 中描述了一些常见的 RGB 三元组。

表 4-4。

Common RGB triplets

| 三个一组 | 颜色 | | --- | --- | | (255, 0, 0) | 红色 | | (0, 255, 0) | 格林(姓氏);绿色的 | | (0, 0, 255) | 蓝色 | | (0, 0, 0) | 黑色 | | (255, 255, 255) | 白色的 |

另一种颜色空间是灰度,从技术上讲根本不是颜色空间,因为它丢弃了颜色信息。它存储的只是每个像素的亮度,通常是一个 8 位无符号整数。还有许多其他颜色空间,其中值得注意的是 YUV、CMYK 和 LAB。(你可以在维基百科上读到它们。)

如前所述,通过 imread()分别使用 CV_LOAD_IMAGE_COLOR 和 CV _ LOAD _ IMAGE _ gray 标志,可以在 RGB 或灰度颜色空间中加载图像。然而,如果你已经加载了一个图像,OpenCV 有转换它的颜色空间的函数。出于各种原因,您可能希望在色彩空间之间进行转换。一个常见的原因是,YUV 颜色空间中的 U 和 V 通道编码所有颜色信息,但对照明或亮度不变。因此,如果你想对你的图像进行一些处理,需要光照不变,你应该转移到 YUV 颜色空间,并使用 U 和 V 通道(Y 通道专门存储强度信息)。注意,没有一个 R、G 或 B 通道是光照不变的。

函数 cvtColor()进行颜色空间转换。例如,要将 img1 中的 RGB 图像转换为灰度图像,您需要:

cvtColor(img1, img2, CV_RGB2GRAY);

其中 CV_RGB2GRAY 是一个预定义的代码,它告诉 OpenCV 要执行哪个转换。这个函数可以在许多颜色空间之间转换,你可以在 OpenCV 文档页面( http://docs.opencv.org/modules/imgproc/doc/miscellaneous_transformations.html?highlight=cvtcolor#cv.CvtColor )上阅读更多关于它的内容。

GUI 跟踪条和回调函数

本节将向您介绍 OpenCV highgui模块的一个非常有用的特性——跟踪条或滑块,以及操作它们所必需的回调函数。我们将使用滑块将图像从 RGB 颜色转换为灰度,反之亦然,因此希望这也将强化您的颜色空间转换概念。

回调函数

回调函数是事件发生时自动调用的函数。它们可以与 OpenCV 中的各种 GUI 事件相关联,比如单击鼠标左键或右键、移动滑块等等。对于我们的颜色空间转换应用,我们将把回调函数与滑块的移动关联起来。每当用户移动滑块时,这个函数就会被自动调用。简而言之,该函数检查滑块的值,并在相应地转换其颜色空间后显示图像。虽然这听起来很复杂,但是 OpenCV 让它变得非常简单。让我们看看清单 4-2 中的代码。

清单 4-2。色彩空间转换

// Function to change between color and grayscale representations of an image using a GUI trackbar

// Author: Samarth Manoj Brahmbhatt, University of Pennsylvania

#include <iostream>

#include <opencv2/highgui/highgui.hpp>

#include <opencv2/imgproc/imgproc.hpp>

using namespace std;

using namespace cv;

// Global variables

const int slider_max = 1;

int slider;

Mat img;

// Callback function for trackbar event

void on_trackbar(int pos, void *)

{

Mat img_converted;

if(pos > 0) cvtColor(img, img_converted, CV_RGB2GRAY);

else img_converted = img;

imshow("Trackbar app", img_converted);

}

int main()

{

img = imread("image.jpg");

namedWindow("Trackbar app");

imshow("Trackbar app", img);

slider = 0;

createTrackbar("RGB <-> Grayscale", "Trackbar app", &slider, slider_max, on_trackbar);

while(char(waitKey(1)) != 'q') {}

return 0;

}

像往常一样,我将把代码分成几个部分来解释。

台词

const int slider_max = 1;

int slider;

Mat img;

声明保存原始图像、滑块位置和最大可能滑块位置的全局变量。因为我们只需要滑块的两个选项——颜色和灰度(0 和 1 ),并且最小可能的滑块位置总是 0,所以我们将最大滑块位置设置为 1。全局变量是必要的,这样两个函数都可以访问它们。

台词

img = imread("image.jpg");

namedWindow("Trackbar app");

imshow("Trackbar app", img);

在主函数中,只需读取一个名为image.jpg的彩色图像,创建一个名为“Trackbar app”的窗口(创建跟踪条需要一个窗口),并在窗口中显示图像。

createTrackbar("RGB <-> Grayscale", "Trackbar app", &slider, slider_max, on_trackbar);

在我们之前创建的名为“跟踪条应用”的窗口中创建一个名为“RGB 灰度”的跟踪条(你应该在 OpenCV 文档中查找这个函数)。我们还通过使用&slider、跟踪条的最大可能值以及将名为on_trackbar的回调函数与跟踪条事件相关联,传递一个指向保存跟踪条起始值的变量的指针。

现在让我们看看回调函数on_trackbar(),,它(对于跟踪条回调)必须总是类型void foo(int. void *).。这里的变量pos保存跟踪条的值,每次用户滑动跟踪条时,这个函数将被调用,并更新pos的值。台词

if(pos > 0) cvtColor(img, img_converted, CV_RGB2GRAY);

else img_converted = img;

imshow("Trackbar app", img_converted);

只需检查pos的值,并在之前创建的窗口中显示正确的图像。

编译并运行您的色彩空间转换器应用,如果一切顺利,您应该会看到它的运行,如图 4-1 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-1。

The color-space conversion app in action

ROI:从图像中裁剪出一个矩形部分

在本节中,您将了解感兴趣区域。然后,您将使用这些知识制作一个应用,允许您选择图像中的矩形部分并将其裁剪掉。

图像中的感兴趣区域

感兴趣的区域正是它听起来的样子。这是我们特别感兴趣的图像区域,并且我们希望将我们的处理集中在这一区域上。它主要用于图像太大并且图像的所有部分都与我们的使用无关的情况;或者处理操作太重,以至于将其应用于整个图像在计算上是禁止的。通常 ROI 被指定为矩形。在 OpenCV 中,使用rect结构指定矩形 ROI(同样,在 OpenCV 文档中查找rect)。我们需要左上角的位置,宽度和高度来定义一个rect

让我们看一下我们的应用的代码(清单 4-3 ),然后一次分析一点。

清单 4-3。从图像中裁剪出一部分

// Program to crop images using GUI mouse callbacks

// Author: Samarth Manoj Brahmbhatt, University of Pennsylvania

#include <iostream>

#include <opencv2/highgui/highgui.hpp>

#include <opencv2/imgproc/imgproc.hpp>

using namespace std;

using namespace cv;

// Global variables

// Flags updated according to left mouse button activity

bool ldown = false, lup = false;

// Original image

Mat img;

// Starting and ending points of the user's selection

Point corner1, corner2;

// ROI

Rect box;

// Callback function for mouse events

static void mouse_callback(int event, int x, int y, int, void *)

{

// When the left mouse button is pressed, record its position and save it in corner1

if(event == EVENT_LBUTTONDOWN)

{

ldown = true;

corner1.x = x;

corner1.y = y;

cout << "Corner 1 recorded at " << corner1 << endl;

}

// When the left mouse button is released, record its position and save it in corner2

if(event == EVENT_LBUTTONUP)

{

// Also check if user selection is bigger than 20 pixels (jut for fun!)

if(abs(x - corner1.x) > 20 && abs(y - corner1.y) > 20)

{

lup = true;

corner2.x = x;

corner2.y = y;

cout << "Corner 2 recorded at " << corner2 << endl << endl;

}

else

{

cout << "Please select a bigger region" << endl;

ldown = false;

}

}

// Update the box showing the selected region as the user drags the mouse

if(ldown == true && lup == false)

{

Point pt;

pt.x = x;

pt.y = y;

Mat local_img = img.clone();

rectangle(local_img, corner1, pt, Scalar(0, 0, 255));

imshow("Cropping app", local_img);

}

// Define ROI and crop it out when both corners have been selected

if(ldown == true && lup == true)

{

box.width = abs(corner1.x - corner2.x);

box.height = abs(corner1.y - corner2.y);

box.x = min(corner1.x, corner2.x);

box.y = min(corner1.y, corner2.y);

// Make an image out of just the selected ROI and display it in a new window

Mat crop(img, box);

namedWindow("Crop");

imshow("Crop", crop);

ldown = false;

lup = false;

}

}

int main()

{

img = imread("image.jpg");

namedWindow("Cropping app");

imshow("Cropping app", img);

// Set the mouse event callback function

setMouseCallback("Cropping app", mouse_callback);

// Exit by pressing 'q'

while(char(waitKey(1)) != 'q') {}

return 0;

}

这段代码目前看起来可能很大,但是,正如您可能已经意识到的,它的大部分只是鼠标事件的逻辑处理。在队列中

setMouseCallback("Cropping app", mouse_callback);

在主函数中,我们将鼠标回调设置为名为mouse_callback的函数。函数mouse_callback主要完成以下工作:

  • 记录按下左键时鼠标的(x,y)位置。
  • 记录释放左键时鼠标的(x,y)位置。
  • 当这两项操作完成后,在图像中定义一个 ROI,并在另一个窗口中显示仅由 ROI 组成的另一个图像(您可以添加一个保存 ROI 的功能—为此使用 imwrite())。
  • 绘制用户选择,并在用户拖动鼠标并按下左键时保持更新。

实现非常简单,一目了然。我想把重点放在这个程序中引入的三个新的编程特性上:Pointrect,以及通过指定一个rect ROI 从另一个图像创建一个图像。

Point结构用于存储关于一个点的信息,在我们的例子中是用户选择的角。该结构有两个数据成员,都是int,称为xy。其他的点结构如Point3dPoint2dPoint3f也存在于 OpenCV 中,你应该在 OpenCV 文档中查看它们。

rect结构用于存储一个矩形的信息,使用它的xywidthheight. xy这里是图像中矩形左上角的坐标。

如果名为rrect保存了图像M1中 ROI 的信息,您可以使用

Mat M2(M1, r);

裁剪应用看起来如图 4-2 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-2。

The cropping app in action

访问图像的单个像素

有时有必要访问图像或其 ROI 中各个像素的值。OpenCV 有有效的方法做到这一点。要访问cv::Mat图像中位置(i, j)处的像素,可以使用cv::Matat()属性,如下所示:

对于每个像素都是 8 位无符号字符的灰度图像M,使用M.at<uchar>(i, j).

对于 3 通道(RGB)图像M,其中每个像素是 3 个 8 位无符号字符的向量,使用M.at<Vec3b>[c],其中c是通道号,从 0 到 2。

锻炼

能否根据目前所学的概念,制作一个非常简单的彩色图像分割 app?

分割意味着识别图像的不同部分。这里的零件是用颜色来定义的。我们希望识别图像中的红色区域:给定一个彩色图像,您应该产生一个黑白图像输出,其像素在原始图像的红色区域为 255(开),在非红色区域为 0(关)。

您遍历彩色图像中的像素,检查它们的红色值是否在某个范围内。如果是,则打开输出图像的相应像素。你当然可以用简单的方法,遍历图像中的所有像素。但是看看你是否能在 OpenCV 文档中找到一个为你做完全相同任务的函数。也许你甚至可以做一个跟踪条来动态调整这个范围!

录像

OpenCV 中的视频通过 FFMPEG 支持进行处理。在继续本节中的代码之前,请确保您已经安装了支持 FFMPEG 的 OpenCV。

显示来自网络摄像头或 USB 摄像头/文件的源

让我们检查一段很短的代码(清单 4-4 ),它将显示来自你的计算机的默认摄像设备的视频。对于大多数笔记本电脑来说,这是集成的网络摄像头。

清单 4-4。显示来自默认摄像机设备的视频源

// Program to display a video from attached default camera device

// Author: Samarth Manoj Brahmbhatt, University of Pennsylvania

#include <opencv2/opencv.hpp>

using namespace cv;

using namespace std;

int main()

{

// 0 is the ID of the built-in laptop camera, change if you want to use other camera

VideoCapture cap(0);

//check if the file was opened properly

if(!cap.isOpened())

{

cout << "Capture could not be opened successfully" << endl;

return -1;

}

namedWindow("Video");

// Play the video in a loop till it ends

while(char(waitKey(1)) != 'q' && cap.isOpened())

{

Mat frame;

cap >> frame;

// Check if the video is over

if(frame.empty())

{

cout << "Video over" << endl;

break;

}

imshow("Video", frame);

}

return 0;

}

代码本身是不言自明的,我只想简单介绍几行代码。

VideoCapture cap(0);

这将创建一个 VideoCapture 对象,该对象链接到您计算机上的设备编号 0(默认设备)。和

cap >> frame;

从 VideoCapture 对象 cap 链接到的设备中提取帧。还有一些其他方法可以从相机设备中提取帧,特别是当您有多个相机并且想要同步它们时(同时从所有相机中提取帧)。我将在第十章中介绍这些方法。

您也可以给 VideoCapture 构造函数一个文件名,OpenCV 将以完全相同的方式为您播放该文件中的视频(参见清单 4-5)。

清单 4-5。显示文件中视频的程序

// Program to display a video from a file

// Author: Samarth Manoj Brahmbhatt, University of Pennsylvania

// Video from:http://ftp.nluug.nl/ftp/graphics/blender/apricot/trailer/sintel_trailer-480p.mp4

#include <opencv2/opencv.hpp>

using namespace cv;

using namespace std;

int main()

{

// Create a VideoCapture object to read from video file

VideoCapture cap("video.mp4");

//check if the file was opened properly

if(!cap.isOpened())

{

cout << "Capture could not be opened succesfully" << endl;

return -1;

}

namedWindow("Video");

// Play the video in a loop till it ends

while(char(waitKey(1)) != 'q' && cap.isOpened())

{

Mat frame;

cap >> frame;

// Check if the video is over

if(frame.empty())

{

cout << "Video over" << endl;

break;

}

imshow("Video", frame);

}

return 0;

}

将视频写入磁盘

一个VideoWriter对象用于将视频写入磁盘。此类的构造函数需要以下内容作为输入:

  • 输出文件名
  • 输出文件的编解码器。在下面的代码中,我们使用 MPEG 编解码器,这是非常常见的。您可以使用CV_FOURCC宏指定编解码器。各种编解码器的四个字符代码可以在 www.fourcc.org/codecs.php 找到。请注意,要使用编解码器,您必须在计算机上安装该编解码器
  • 帧频
  • 框架尺寸

你可以得到一个视频的各种属性(像帧大小,帧速率,亮度,对比度,曝光度等。)从一个VideoCapture对象使用get()函数。在清单 4-6 中,它将视频从默认的摄像机设备写入磁盘,我们使用get()函数来获取帧的大小。如果你的相机支持的话,你也可以用它来获得帧速率。

清单 4-6。将视频从默认摄像机设备源写入磁盘的代码

// Program to write video from default camera device to file

// Author: Samarth Manoj Brahmbhatt, University of Pennsylvania

#include <opencv2/opencv.hpp>

using namespace cv;

using namespace std;

int main()

{

// 0 is the ID of the built-in laptop camera, change if you want to use other camera

VideoCapture cap(0);

//check if the file was opened properly

if(!cap.isOpened())

{

cout << "Capture could not be opened succesfully" << endl;

return -1;

}

// Get size of frames

Size S = Size((int) cap.get(CV_CAP_PROP_FRAME_WIDTH), (int) cap.get(CV_CAP_PROP_FRAME_HEIGHT));

// Make a video writer object and initialize it at 30 FPS

VideoWriter put("output.mpg", CV_FOURCC('M','P','E','G'), 30, S);

if(!put.isOpened())

{

cout << "File could not be created for writing. Check permissions" << endl;

return -1;

}

namedWindow("Video");

// Play the video in a loop till it ends

while(char(waitKey(1)) != 'q' && cap.isOpened())

{

Mat frame;

cap >> frame;

// Check if the video is over

if(frame.empty())

{

cout << "Video over" << endl;

break;

}

imshow("Video", frame);

put << frame;

}

return 0;

}

摘要

在这一章中,您接触了大量的 OpenCV 代码,并且看到了编写复杂任务的程序是多么容易,比如在 OpenCV 中显示视频。这一章没有很多计算机视觉。它的目的是向您介绍 OpenCV 的来龙去脉。下一章将处理图像过滤和转换,并将利用你在这里学到的编程概念。

五、过滤图像

Abstract

在本章中,我们将继续讨论对图像的基本操作。特别是,我们将讨论一些滤波器理论和不同种类的滤波器,您可以将它们应用于图像,以便提取各种信息或抑制各种噪声。

在本章中,我们将继续讨论对图像的基本操作。特别是,我们将讨论一些滤波器理论和不同种类的滤波器,您可以将它们应用于图像,以便提取各种信息或抑制各种噪声。

图像处理和计算机视觉之间只有一线之隔。图像处理主要处理通过以各种方式变换图像来获得图像的不同表示。通常(但不总是)这样做是为了“查看”,例如更改图像的色彩空间、锐化或模糊图像、更改对比度、仿射变换、裁剪、调整大小等等。相比之下,计算机视觉关注的是从图像中提取信息,以便人们做出决策。通常,必须从有噪声的图像中提取信息,因此还必须分析噪声,并想办法抑制噪声,同时不会过多影响图像的相关信息内容。

举个例子,一个问题是你必须制造一个简单的轮式自动机器人,它可以向一个方向移动,跟踪并拦截一个红色的球。

这里的计算机视觉问题是双重的:查看从机器人上的摄像头获取的图像中是否有一个红球,如果有,知道它沿着机器人的运动方向相对于机器人的位置。请注意,这两个都是决定性的信息,基于这些信息,机器人可以决定是否移动,如果是,向哪个方向移动。

滤镜是最基本的操作,您可以对图像执行这些操作来提取信息。(它们可能极其复杂。但是我们将从简单的开始。)为了让您对本章的内容有一个大致的了解,我们将首先从一些图像滤镜理论开始,然后研究一些简单的滤镜。在许多计算机视觉流水线中,应用这些过滤器可以作为有用的预处理或后处理步骤。这些操作包括:

  • 模糊
  • 上下调整图像大小
  • 侵蚀和扩张
  • 检测边缘和拐角

然后,我们将讨论如何有效地检查图像中像素值的界限。利用这一新发现的知识,我们将制作第一个非常简单的 objector 探测器应用。接下来将讨论打开和关闭的图像形态学操作,这是从图像中消除噪声的有用工具(我们将通过向我们的 object detector 应用添加打开和关闭步骤来消除噪声来演示这一点)。

图像过滤器

滤波器只不过是一个函数,它获取信号的局部值,并给出以某种方式与信号中包含的信息成比例的输出。通常,一个“滑动”滤波器通过信号。为了明确这两个重要的陈述,考虑下面的一维时变信号,它可以是一个城市每天的温度(或类似的东西)。

我们想要提取的信息是温度波动;具体来说,我们想看看每天的气温变化有多剧烈。所以我们做了一个过滤函数,它给出了今天的温度和昨天的温度之差的绝对值。我们遵循的等式是 y[n]= | x[n]—x[n 1]|,其中 y[n]是第 n 天的滤波器输出,x[n]是信号,即第 n 天的城市温度。

这个滤波器(长度为 2)在信号中“滑动”,输出类似于图 5-1 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-1。

A simple signal (above) and output of a differential filter on that signal (below)

正如您所观察到的,滤波器增强了信号的差异。简单地说,如果今天的温度与昨天有很大不同,那么今天的过滤器输出将会更高。如果今天的温度和昨天差不多,那么今天的滤波器输出几乎为零。希望这个非常简单的例子能让您相信,滤波器设计基本上就是计算出一个函数,它将接收信号值,并增强其中所选的信息内容。还有一些其他的条件和规则需要注意,但是对于我们简单的应用,我们可以忽略它们。

现在让我们继续讨论图像滤波器,它与我们之前讨论的 1-D 滤波器有些不同,因为图像信号是 2-D 的,因此滤波器也必须是 2-D 的(如果我们想考虑所有四边的像素邻居)。检测图像中垂直边缘的示例过滤器将帮助您更好地理解。第一步是确定滤波器矩阵。筛选器矩阵是筛选器函数的离散化版本,使在计算机上应用筛选器成为可能。它们的长度和宽度通常是奇数,因此可以明确地确定中心元素。对于我们检测垂直边缘的情况,矩阵非常简单:

0 0 0

-1 2 -1

0 0 0

或者如果我们想考虑两个相邻的像素:

0 0 0 0 0

0 0 0 0 0

-1 -2 6 -2 -1

0 0 0 0 0

0 0 0 0 0

现在,让我们滑动这个过滤器通过一个图像,看看它是否工作!在此之前,我必须详细说明什么是“应用”一个过滤器矩阵(或内核)的图像意味着什么。内核放在图像上,通常从图像的左上角开始。每次迭代都会执行以下步骤:

  • 在内核的元素和由内核覆盖的图像的像素之间执行逐元素乘法
  • 函数用于使用所有这些元素乘法的结果来计算一个数字。这个函数可以是总和、平均值、最小值、最大值或者非常复杂的东西。如此计算的值被称为图像在该迭代中对滤波器的“响应”
  • 位于内核中心元素下方的像素采用响应值
  • 内核向右移动,必要时向下移动

用这个过滤器矩阵(也称为内核)过滤一个仅由水平和垂直边缘组成的图像,得到如图 5-2 所示的过滤图像。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-2。

A simple image with horizontal and vertical edges (left) and output of filtering with kernel (right)

OpenCV 有一个名为filter2D()的函数,我们可以使用它进行高效的基于内核的过滤。要了解如何使用它,请研究前面讨论的用于过滤的代码,并阅读它的文档。这个函数非常强大,因为它允许您通过指定的任何内核过滤图像。清单 5-1 展示了这个函数的使用。

清单 5-1。程序应用一个简单的过滤器矩阵来检测图像的水平边缘

// Program to apply a simple filter matrix to an image

// Author: Samarth Manoj Brahmbhatt, University of Pennsylvania

#include <opencv2/opencv.hpp>

#include <opencv2/highgui/highgui.hpp>

using namespace std;

using namespace cv;

int main() {

Mat img = imread("image.jpg", CV_LOAD_IMAGE_GRAYSCALE), img_filtered;

// Filter kernel for detecting vertical edges

float vertical_fk[5][5] = {{0,0,0,0,0}, {0,0,0,0,0}, {-1,-2,6,-2,-1}, {0,0,0,0,0}, {0,0,0,0,0}};

// Filter kernel for detecting horizontal edges

float horizontal_fk[5][5] = {{0,0,-1,0,0}, {0,0,-2,0,0}, {0,0,6,0,0}, {0,0,-2,0,0}, {0,0,-1,0,0}};

Mat filter_kernel = Mat(5, 5, CV_32FC1, horizontal_fk);

// Apply filter

filter2D(img, img_filtered, -1, filter_kernel);

namedWindow("Image");

namedWindow("Filtered image");

imshow("Image", img);

imshow("Filtered image", img_filtered);

// imwrite("filtered_image.jpg", img_filtered);

while(char(waitKey(1)) != 'q') {}

return 0;

}

您可能已经猜到,检测垂直边缘的内核是:

0 0 -1 0 0

0 0 -2 0 0

0 0 6 0 0

0 0 -2 0 0

0 0 -1 0 0

它可以很好地检测垂直边缘,如图 5-3 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-3。

Detecting vertical edges in an image

尝试制作不同的检测器内核和实验各种图像是很有趣的!

如果您给多通道彩色图像作为filter2D()的输入,它将相同的内核矩阵应用于所有通道。有时,您可能希望只检测某个通道中的边缘,或者对不同的通道使用不同的核,并选择最强的边缘(或平均边缘强度)。在这种情况下,您应该使用split()函数分割图像,并分别应用内核。

不要忘记查看 OpenCV 文档中所有你正在学习的新函数!

因为边缘检测是计算机视觉中非常重要的操作,所以已经进行了大量的研究来设计可以在任意方向检测边缘的方法和智能滤波器矩阵。OpenCV 提供了其中一些算法的实现,在这一章的后面我会有更多的介绍。同时,让我们坚持我们的章节计划,讨论第一个图像预处理步骤,模糊。

模糊图像

模糊图像是在不改变图像外观的情况下缩小图像尺寸的第一步。模糊可以被认为是低通滤波操作,并使用简单直观的核矩阵来完成。可以认为一幅图像沿其两个轴的方向都有不同的“频率成分”。边缘具有高频率,而缓慢变化的强度值具有低频率。更具体地说,垂直边缘沿着图像的水平轴产生高频分量,反之亦然。精细纹理区域也具有高频率(请注意,如果一个区域中的像素强度值在短像素距离内变化很大,则该区域称为精细纹理区域)。较小的图像不能很好地处理高频。

可以这样想:假设你有一张纹理清晰的 640 x 480 的图片。您无法在 320 x 240 的图像中保持所有这些短时间间隔的高强度像素值变化,因为它只有像素数量的四分之一。所以无论何时你想缩小一幅图像的尺寸,你都应该从中去除高频成分。换句话说,模糊它。平滑掉那些高量级的短间隔变化。如果在调整大小之前没有模糊,您可能会在调整大小后的图像中看到伪像。原因很简单,取决于信号理论的一个基本定理,即采样一个信号会导致该信号的频域出现无限重复。因此,如果信号有许多高频成分,重复频域表示的边缘会相互干扰。一旦发生这种情况,信号就无法准确恢复。这里,信号是我们的图像,调整大小是通过移除行和列来完成的,也就是下采样。这种现象被称为混叠。如果你想了解更多,你应该能够在任何好的数字信号处理资源中找到详细的解释。因为模糊去除了图像中的高频成分,所以有助于避免混叠。

当您想要增加图像的大小时,模糊也是一个重要的后处理步骤。如果您想要将图像的大小增加一倍,可以为每一行(列)添加一个空白行(列),然后模糊生成的图像,使空白行(列)的外观与相邻行(列)相似。

模糊可以通过用图像周围区域中像素的某种平均值替换图像中的每个像素来实现。为了有效地做到这一点,该区域保持矩形并围绕像素对称,并且图像与“归一化”核进行卷积(归一化是因为我们想要平均值,而不是总和)。一个非常简单的内核是箱式内核:

1 1 1 1 1

1 1 1 1 1

1 1 1 1 1

1 1 1 1 1

1 1 1 1 1

这个内核认为每个像素都同等重要。一个更好的核应该是随着像素与中心像素的距离的增加而减少像素的影响。高斯核可以做到这一点,并且是最常用的模糊核:

1 4 6 4 1

4 16 24 16 4

6 24 36 24 6

4 16 24 16 4

1 4 6 4 1

一种方法是通过除以所有元素的总和来“归一化”核,25 用于盒核,256 用于高斯核。您可以使用 OpenCV 函数getGaussianKernel()创建不同大小的高斯内核。查看这个函数的文档,了解 OpenCV 用来计算内核的公式。您可以将这些内核插入到清单 5-1 中,以模糊一些图像(不要忘记将内核除以其元素的总和)。然而,OpenCV 也提供了更高级的函数GaussianBlur(),它只是将高斯函数的核大小和方差作为输入,并为我们完成所有其他工作。我们在清单 5-2 的代码中使用了这个函数,它用一个滑块指示大小的高斯核来模糊一个图像。它应该有助于你实际理解模糊。图 5-4 显示了运行中的代码。

清单 5-2。使用不同大小的高斯核交互式模糊图像的程序

// Program to interactively blur an image using a Gaussian kernel of varying size

// Author: Samarth Manoj Brahmbhatt, University of Pennyslvania

#include <opencv2/opencv.hpp>

#include <opencv2/highgui/highgui.hpp>

#include <opencv2/imgproc/imgproc.hpp>

using namespace std;

using namespace cv;

Mat image, image_blurred;

int slider = 5;

float sigma = 0.3 * ((slider - 1) * 0.5 - 1) + 0.8;

void on_trackbar(int, void *) {

int k_size = max(1, slider);

k_size = k_size % 2 == 0 ? k_size + 1 : k_size;

setTrackbarPos("Kernel Size", "Blurred image", k_size);

sigma = 0.3 * ((k_size - 1) * 0.5 - 1) + 0.8;

GaussianBlur(image, image_blurred, Size(k_size, k_size), sigma);

imshow("Blurred image", image_blurred);

}

int main() {

image = imread("baboon.jpg");

namedWindow("Original image");

namedWindow("Blurred image");

imshow("Original image", image);

sigma = 0.3 * ((slider - 1) * 0.5 - 1) + 0.8;

GaussianBlur(image, image_blurred, Size(slider, slider), sigma);

imshow("Blurred image", image_blurred);

createTrackbar("Kernel Size", "Blurred image", &slider, 21, on_trackbar);

while(char(waitKey(1) != 'q')) {}

return 0;

}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-4。

Blurring an image with a Gaussian kernel

请注意我们使用的基于内核大小计算方差的启发式公式,以及使用setTrackbarPos()函数强制内核大小为奇数且大于 0 的工具栏“锁定”机制。

上下调整图像大小

现在我们知道了在调整图像大小时模糊图像的重要性,我们准备好调整图像大小并验证我所阐述的理论是否正确。

你可以通过使用如图 5-5 所示的resize()函数来做一个简单的几何尺寸调整(简单地抛出行和列)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-5。

Aliasing artifacts—simple geometric resizing of an image down by a factor of 4

观察由于混叠而在调整大小的图像中产生的伪像。pyrDown()函数通过高斯核模糊图像,并将其缩小 2 倍。图 5-6 中的图像是原始图像的四倍缩小版,通过使用两次pyrDown()获得(注意没有混叠伪影)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-6。

Avoiding aliasing while resizing images by first blurring them with a Gaussian kernel

如果你想放大一幅图像,函数resize()也是有效的,它采用了一系列你可以选择的插值技术。如果您想在放大后模糊图像,请使用pyrUp()功能适当的次数(因为它的工作系数是 2)。

侵蚀和扩张图像

腐蚀和膨胀是图像的两种基本形态学操作。顾名思义,形态学运算作用于图像的形式和结构。

侵蚀是通过在图像上滑动所有 1 的矩形核(盒核)来完成的。响应被定义为内核元素和属于内核的像素之间的所有元素乘法的最大值。因为所有的核元素都是一,所以应用这个核意味着用围绕像素的矩形区域中的最小值替换每个像素值。您可以想象这将导致图像中的黑色区域“侵占”到白色区域(因为白色的像素值高于黑色)。

放大图像是相同的,唯一的区别是响应被定义为元素方式乘法的最大值而不是最小值。这将导致白色区域侵占黑色区域。

内核的大小决定了腐蚀或膨胀的程度。清单 5-3 制作了一个在腐蚀和膨胀之间切换的应用,并允许您选择内核的大小(在形态学操作的上下文中也称为结构化元素)。

清单 5-3。检查图像腐蚀和膨胀的程序

// Program to examine erosion and dilation of images

// Author: Samarth Manoj Brahmbhatt, University of Pennyslvania

#include <opencv2/opencv.hpp>

#include <opencv2/highgui/highgui.hpp>

#include <opencv2/imgproc/imgproc.hpp>

using namespace std;

using namespace cv;

Mat image, image_processed;

int choice_slider = 0, size_slider = 5; // 0 - erode, 1 - dilate

void process() {

Mat st_elem = getStructuringElement(MORPH_RECT, Size(size_slider, size_slider));

if(choice_slider == 0) {

erode(image, image_processed, st_elem);

}

else {

dilate(image, image_processed, st_elem);

}

imshow("Processed image", image_processed);

}

void on_choice_slider(int, void *) {

process();

}

void on_size_slider(int, void *) {

int size = max(1, size_slider);

size = size % 2 == 0 ? size + 1 : size;

setTrackbarPos("Kernel Size", "Processed image", size);

process();

}

int main() {

image = imread("j.png");

namedWindow("Original image");

namedWindow("Processed image");

imshow("Original image", image);

Mat st_elem = getStructuringElement(MORPH_RECT, Size(size_slider, size_slider));

erode(image, image_processed, st_elem);

imshow("Processed image", image_processed);

createTrackbar("Erode/Dilate", "Processed image", &choice_slider, 1, on_choice_slider);

createTrackbar("Kernel Size", "Processed image", &size_slider, 21, on_size_slider);

while(char(waitKey(1) != 'q')) {}

return 0;

}

图 5-7 显示了用户使用滑块指定的不同侵蚀和放大量的图像。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-7。

Eroding and dilating images

除了函数erode()dilate()之外,这段代码中值得注意的是函数getStructuralElement() ,,它返回指定形状和大小的结构元素(核矩阵)。预定义的形状包括矩形、椭圆形和十字形。您甚至可以创建自定义形状。所有这些形状都嵌入在一个由零组成的矩形矩阵中返回(属于该形状的元素是 1)。

有效检测图像中的边缘和角点

您之前已经看到,使用滤波器可以很容易地检测出垂直和水平边缘。如果你构建了一个合适的内核,你可以检测任何方向的边缘,只要它是一个固定的方向。然而,在实践中,人们必须在同一图像中检测所有方向的边缘。我们将讨论一些智能的方法来做到这一点。也可以通过使用适当种类的核来检测角点。

优势

边缘是图像中图像梯度非常高的点。我们所说的梯度是指像素强度值的变化。通过计算 X 和 Y 方向上的梯度,然后使用毕达哥拉斯定理将它们结合,来计算图像的梯度。虽然通常不需要,但是您可以通过分别取 Y 和 X 方向的梯度比的反正切来计算梯度的角度。

x 和 Y 方向梯度分别通过用以下核卷积图像来计算:

-3 0 3

-10 0 10

-3 0 3 (for X direction)

-3 -10 -3

0 0 0

3 10 3 (for Y direction)

整体梯度,G = sqrt(Gx 2 + Gy 2

倾斜角,ф=反正切(Gy / Gx)

上面显示的两个内核被称为 Scharr 操作符,OpenCV 提供了一个名为 Scharr()的函数,它将指定大小和指定方向(X 或 Y)的 Scharr 操作符应用于图像。所以让我们制作如清单 5-4 所示的 Scharr 边缘检测程序。

清单 5-4。使用 Scharr 算子检测图像边缘的程序

// Program to detect edges in an image using the Scharr operator

// Author: Samarth Manoj Brahmbhatt, University of Pennyslvania

#include <opencv2/opencv.hpp>

#include <opencv2/highgui/highgui.hpp>

#include <opencv2/imgproc/imgproc.hpp>

using namespace std;

using namespace cv;

int main() {

Mat image = imread("lena.jpg"), image_blurred;

// Blur image with a Gaussian kernel to remove edge noise

GaussianBlur(image, image_blurred, Size(3, 3), 0, 0);

// Convert to gray

Mat image_gray;

cvtColor(image_blurred, image_gray, CV_RGB2GRAY);

// Gradients in X and Y directions

Mat grad_x, grad_y;

Scharr(image_gray, grad_x, CV_32F, 1, 0);

Scharr(image_gray, grad_y, CV_32F, 0, 1);

// Calculate overall gradient

pow(grad_x, 2, grad_x);

pow(grad_y, 2, grad_y);

Mat grad = grad_x + grad_y;

sqrt(grad, grad);

// Display

namedWindow("Original image");

namedWindow("Scharr edges");

// Convert to 8 bit depth for displaying

Mat edges;

grad.convertTo(edges, CV_8U);

imshow("Original image", image);

imshow("Scharr edges", edges);

while(char(waitKey(1)) != 'q') {}

return 0;

}

图 5-8 显示了美丽的莉娜图像的沙尔边缘。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-8。

Scharr edge detector

您可以看到 Scharr 操作符像承诺的那样找到了梯度。然而,在边缘图像中有许多噪声。因为边缘图像具有 8 位深度,所以您可以用 0 到 255 之间的一个数字对其进行阈值处理,以消除噪声。像往常一样,你可以制作一个带有滑块的应用。该应用的代码如清单 5-5 所示,不同阈值的阈值 Scharr 输出如图 5-9 所示。

清单 5-5。使用阈值 Scharr 算子检测图像边缘的程序

// Program to detect edges in an image using the thresholded Scharr operator

// Author: Samarth Manoj Brahmbhatt, University of Pennyslvania

#include <opencv2/opencv.hpp>

#include <opencv2/highgui/highgui.hpp>

#include <opencv2/imgproc/imgproc.hpp>

using namespace std;

using namespace cv;

Mat edges, edges_thresholded;

int slider = 50;

void on_slider(int, void *) {

if(!edges.empty()) {

Mat edges_thresholded;

threshold(edges, edges_thresholded, slider, 255, THRESH_TOZERO);

imshow("Thresholded Scharr edges", edges_thresholded);

}

}

int main() {

//Mat image = imread("lena.jpg"), image_blurred;

Mat image = imread("lena.jpg"), image_blurred;

// Blur image with a Gaussian kernel to remove edge noise

GaussianBlur(image, image_blurred, Size(3, 3), 0, 0);

// Convert to gray

Mat image_gray;

cvtColor(image_blurred, image_gray, CV_BGR2GRAY);

// Gradients in X and Y directions

Mat grad_x, grad_y;

Scharr(image_gray, grad_x, CV_32F, 1, 0);

Scharr(image_gray, grad_y, CV_32F, 0, 1);

// Calculate overall gradient

pow(grad_x, 2, grad_x);

pow(grad_y, 2, grad_y);

Mat grad = grad_x + grad_y;

sqrt(grad, grad);

// Display

namedWindow("Original image");

namedWindow("Thresholded Scharr edges");

// Convert to 8 bit depth for displaying

grad.convertTo(edges, CV_8U);

threshold(edges, edges_thresholded, slider, 255, THRESH_TOZERO);

imshow("Original image", image);

imshow("Thresholded Scharr edges", edges_thresholded);

createTrackbar("Threshold", "Thresholded Scharr edges", &slider, 255, on_slider);

while(char(waitKey(1)) != 'q') {}

return 0;

}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-9。

Scharr edge detector with thresholds of 100 (top) and 200 (bottom)

锐利的边缘

Canny 算法使用一些后处理来清理边缘输出,并给出细而锐利的边缘。计算 Canny 边缘的步骤包括:

  • 通过用大小为 5 的归一化高斯核卷积图像来去除边缘噪声
  • 使用两种不同的内核计算 X 和 Y 梯度:

-1 0 1

-2 0 2

-1 0 1 for X direction and

-1 -2 -1

0 0 0

1 2 1 for Y direction

  • 如前所述,通过毕达哥拉斯定理找到总梯度强度,通过反正切找到梯度角度。角度四舍五入为四个选项:0 度、45 度、90 度和 135 度
  • 非最大抑制:只有当像素的梯度幅度大于其在梯度方向上的相邻像素的梯度幅度时,该像素才被视为在边缘上。这产生了尖锐而薄的边缘
  • 滞后阈值:这个过程使用两个阈值。如果像素的梯度幅度高于上阈值,则该像素被接受为边缘,如果其梯度幅度低于下阈值,则该像素被拒绝。如果梯度幅度在两个阈值之间,则只有当它连接到作为边缘的像素时,它才会被接受为边缘

你可以在 Lena 图片上运行 OpenCV Canny edge 演示,看看 Canny 和 Scharr edges 的区别。图 5-10 显示了从相同的 Lena 照片中提取的 Canny 边缘。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-10。

Canny edge detector

困境

OpenCV 函数goodFeaturesToTrack()实现了一个鲁棒的角点检测器。该算法采用了史和托马西提出的兴趣点检测算法。关于这个函数内部工作的更多信息可以在 http://docs.opencv.org/modules/imgproc/doc/feature_detection.html?highlight=goodfeaturestotrack#goodfeaturestotrack 的文档页面中找到。

该函数接受以下输入:

  • 灰度图像
  • 一个点 2d 的 STL 向量,用来存储角点的位置(稍后会详细介绍 STL 向量)
  • 要返回的最大拐角数。如果算法检测到的角多于这个数量,则只返回最强的适当数量的角
  • 质量水平:拐角的最低可接受质量。拐角的质量被定义为在一个像素处的图像强度梯度矩阵的最小特征值,或者(如果使用哈里斯边角侦测)图像在该像素处对哈里斯函数的响应。有关更多详细信息,请阅读cornerHarris()cornerMinEigenVal()的文档
  • 两个返回角点位置之间的最小欧几里德距离
  • 一个标志,指示是使用哈里斯边角侦测还是最小特征值角检测器(默认为最小特征值)
  • 如果使用哈里斯边角侦测,调整哈里斯检测器的参数(关于该参数的用法,参见cornerHarris()的文档)

STL 是标准模板库的缩写,它提供了非常有用的数据结构,可以模板化成任何数据类型。其中一个数据结构是vector,我们将使用 OpenCV 的Point2dvector来存储角的位置。您可能还记得,Point2d是 OpenCV 存储一对整数值(通常是图像中一个点的位置)的方式。清单 5-6 显示了使用goodFeaturesToTrack()函数从图像中提取角点的代码,允许用户决定角点的最大数量。

清单 5-6。检测图像中的角点的程序

// Program to detect corners in an image

// Author: Samarth Manoj Brahmbhatt, University of Pennyslvania

#include <opencv2/opencv.hpp>

#include <opencv2/highgui/highgui.hpp>

#include <opencv2/imgproc/imgproc.hpp>

#include <stdlib.h>

using namespace std;

using namespace cv;

Mat image, image_gray;

int max_corners = 20;

void on_slider(int, void *) {

if(image_gray.empty()) return;

max_corners = max(1, max_corners);

setTrackbarPos("Max no. of corners", "Corners", max_corners);

float quality = 0.01;

int min_distance = 10;

vector<Point2d> corners;

goodFeaturesToTrack(image_gray, corners, max_corners, quality, min_distance);

// Draw the corners as little circles

Mat image_corners = image.clone();

for(int i = 0; i < corners.size(); i++) {

circle(image_corners, corners[i], 4, CV_RGB(255, 0, 0), -1);

}

imshow("Corners", image_corners);

}

int main() {

image = imread("building.jpg");

cvtColor(image, image_gray, CV_RGB2GRAY);

namedWindow("Corners");

on_slider(0, 0);

createTrackbar("Max. no. of corners", "Corners", &max_corners, 250, on_slider);

while(char(waitKey(1)) != 'q') {}

return 0;

}

在这个应用中,我们通过一个滑块来改变可返回拐角的最大数量。观察我们如何使用circle()函数在角的位置绘制红色填充的小圆圈。app 产生的输出如图 5-11 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-11。

Corners at different values of max_corners

物体探测器应用

我们的第一个对象检测程序将只使用颜色信息。事实上,它更像是一个颜色边界检查程序,而不是严格意义上的对象检测器,因为它不涉及机器学习。这个想法是为了解决我们在本章开始时讨论的问题——找出一个红色球的大致位置,并控制一个简单的轮式机器人拦截它。检测红球最简单的方法是查看图像中像素的 RGB 值是否与红球相对应,这就是我们将要开始的。我们也将在下一章继续学习新技术的同时努力改进这个应用。如果你已经解决了上一章的练习题,你就已经知道使用哪个 OpenCV 函数了。下面是清单 5-7,这是我们在目标检测方面的第一次尝试!

清单 5-7。简单的基于颜色的对象检测器

// Program to display a video from attached default camera device and detect colored blobs using simple // R G and B thresholding

// Author: Samarth Manoj Brahmbhatt, University of Pennsylvania

#include <opencv2/opencv.hpp>

#include <opencv2/highgui/highgui.hpp>

#include <opencv2/imgproc/imgproc.hpp>

using namespace cv;

using namespace std;

Mat frame, frame_thresholded;

int rgb_slider = 0, low_slider = 30, high_slider = 100;

int low_r = 30, low_g = 30, low_b = 30, high_r = 100, high_g = 100, high_b = 100;

void on_rgb_trackbar(int, void *) {

switch(rgb_slider) {

case 0:

setTrackbarPos("Low threshold", "Segmentation", low_r);

setTrackbarPos("High threshold", "Segmentation", high_r);

break;

case 1:

setTrackbarPos("Low threshold", "Segmentation", low_g);

setTrackbarPos("High threshold", "Segmentation", high_g);

break;

case 2:

setTrackbarPos("Low threshold", "Segmentation", low_b);

setTrackbarPos("High threshold", "Segmentation", high_b);

break;

}

}

void on_low_thresh_trackbar(int, void *) {

switch(rgb_slider) {

case 0:

low_r = min(high_slider - 1, low_slider);

setTrackbarPos("Low threshold", "Segmentation", low_r);

break;

case 1:

low_g = min(high_slider - 1, low_slider);

setTrackbarPos("Low threshold", "Segmentation", low_g);

break;

case 2:

low_b = min(high_slider - 1, low_slider);

setTrackbarPos("Low threshold", "Segmentation", low_b);

break;

}

}

void on_high_thresh_trackbar(int, void *) {

switch(rgb_slider) {

case 0:

high_r = max(low_slider + 1, high_slider);

setTrackbarPos("High threshold", "Segmentation", high_r);

break;

case 1:

high_g = max(low_slider + 1, high_slider);

setTrackbarPos("High threshold", "Segmentation", high_g);

break;

case 2:

high_b = max(low_slider + 1, high_slider);

setTrackbarPos("High threshold", "Segmentation", high_b);

break;

}

}

int main()

{

// Create a VideoCapture object to read from video file

// 0 is the ID of the built-in laptop camera, change if you want to use other camera

VideoCapture cap(0);

//check if the file was opened properly

if(!cap.isOpened())

{

cout << "Capture could not be opened succesfully" << endl;

return -1;

}

namedWindow("Video");

namedWindow("Segmentation");

createTrackbar("0\. R\n1\. G\n2.B", "Segmentation", &rgb_slider, 2, on_rgb_trackbar);

createTrackbar("Low threshold", "Segmentation", &low_slider, 255, on_low_thresh_trackbar);

createTrackbar("High threshold", "Segmentation", &high_slider, 255, on_high_thresh_trackbar);

while(char(waitKey(1)) != 'q' && cap.isOpened())

{

cap >> frame;

// Check if the video is over

if(frame.empty())

{

cout << "Video over" << endl;

break;

}

inRange(frame, Scalar(low_b, low_g, low_r), Scalar(high_b, high_g, high_r), frame_thresholded);

imshow("Video", frame);

imshow("Segmentation", frame_thresholded);

}

return 0;

}

图 5-12 显示程序检测一个橙色物体。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-12。

Color-based object detector

观察我们如何使用锁定机制来确保较低的阈值永远不会高于较高的阈值,反之亦然。要使用此应用,首先将物体放在相机前。然后,将鼠标悬停在名为“视频”的窗口中的对象上,观察 R、G 和 B 值。最后,在“分段”窗口中适当调整范围。这个应用有很多缺点:

  • 它不能检测多种颜色的物体
  • 它高度依赖于照明
  • 它会对相同颜色的其他物体产生误报

但这是一个好的开始!

形态学打开和关闭图像以去除噪声

回想一下形态学腐蚀和膨胀的定义。开口是通过腐蚀图像,然后放大图像而获得的。它将具有移除图像中的小白色区域的效果。闭合是通过扩张图像然后腐蚀它来实现的;这将产生相反的效果。这两种操作经常用于从图像中去除噪声。打开会移除白色的小像素,而关闭会移除黑色的小“洞”我们的 object detector 应用是检查这一点的理想平台,因为我们在“分割”窗口中有一些白点和黑点形式的噪声,如图 5-12 所示。

OpenCV 函数morphologyEX()可用于执行高级形态学操作,如打开和关闭图像。因此,我们可以通过在前面的对象检测器代码的main()函数的while循环中添加三行来打开和关闭inRange()函数的输出,以移除黑点和白点。新的main()函数如清单 5-8 所示。

清单 5-8。向对象检测器代码添加打开和关闭步骤

int main()

{

// Create a VideoCapture object to read from video file

// 0 is the ID of the built-in laptop camera, change if you want to use other camera

VideoCapture cap(0);

//check if the file was opened properly

if(!cap.isOpened())

{

cout << "Capture could not be opened succesfully" << endl;

return -1;

}

namedWindow("Video");

namedWindow("Segmentation");

createTrackbar("0\. R\n1\. G\n2.B", "Segmentation", &rgb_slider, 2, on_rgb_trackbar);

createTrackbar("Low threshold", "Segmentation", &low_slider, 255, on_low_thresh_trackbar);

createTrackbar("High threshold", "Segmentation", &high_slider, 255, on_high_thresh_trackbar);

while(char(waitKey(1)) != 'q' && cap.isOpened())

{

cap >> frame;

// Check if the video is over

if(frame.empty())

{

cout << "Video over" << endl;

break;

}

inRange(frame, Scalar(low_b, low_g, low_r), Scalar(high_b, high_g, high_r), frame_thresholded);

Mat str_el = getStructuringElement(MORPH_RECT, Size(3, 3));

morphologyEx(frame_thresholded, frame_thresholded, MORPH_OPEN, str_el);

morphologyEx(frame_thresholded, frame_thresholded, MORPH_CLOSE, str_el);

imshow("Video", frame);

imshow("Segmentation", frame_thresholded);

}

return 0;

}

图 5-13 显示打开和关闭色彩绑定检查器输出确实可以去除斑点和孔洞。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-13。

Removing small patches of noisy pixels by opening and closing

摘要

图像滤波是所有计算机视觉操作的基础。在抽象的意义上,应用于图像的每一种算法都可以被认为是一种过滤操作,因为您试图从图像中包含的大量不同种类的信息中提取一些相关的信息。在这一章中,你学习了很多基于过滤器的图像操作,这将有助于你开始许多复杂的计算机视觉项目。记住,只有当你从图像中提取出决定性的信息时,计算机视觉才是完整的。你还开发了简单的基于颜色的物体探测器应用,我们将在下一章继续。

本章讨论了许多低级算法,而下一章将更多地关注处理图像中区域的形式和结构的算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值