OpenCV是计算机视觉中经典的专用库,其支持多语言、跨平台,功能强大。OpenCV-Python为OpenCV提供了Python接口,使得使用者在Python中能够调用C/C++,在保证易读性和运行效率的前提下,实现所需的功能。OpenCV-Python Tutorials是官方提供的文档,其内容全面、简单易懂,使得初学者能够快速上手使用。
2014年段力辉在当时已翻译过OpenCV3.0,但时隔五年,如今的OpenCV4.1中许多函数和内容已经有所更新,因此有必要对该官方文档再进行一次翻译。
翻译过程中难免有所疏漏,如发现错误,希望大家指出,谢谢支持。
OpenCV-Python Tutorials官方文档:
https://docs.opencv.org/3.4/d6/d00/tutorialpyroot.html
了解如何在计算机上安装OpenCV-Python
在这里,您将学习如何显示和保存图像和视频,控制鼠标事件以及创建轨迹栏。
在本节中,您将学习图像的基本操作、例如像素编辑、几何变换,代码优化、一些数学工具等。
在本节中,您将学习OpenCV内部的不同图像处理函数。
在本节中,您将学习有关特征检测和描述符的信息
在本部分中,您将学习与对象跟踪等视频配合使用的不同技术。
在本节中,我们将学习有关相机校准,立体成像等的信息。
在本节中,您将学习OpenCV内部的不同图像处理函数。
在本节中,您将学习不同的计算摄影技术如图像去噪等。
在本节中,您将学习目标检测技术,例如人脸检测等。
在本节中,我们将了解如何生成OpenCV-Python Binding
OpenCV由Gary Bradsky于1999年在英特尔创立,第一版于2000年问世。Vadim Pisarevsky加入Gary Bradsky,一起管理英特尔的俄罗斯软件OpenCV团队。2005年,OpenCV用于Stanley,该车赢得了2005年DARPA挑战赛的冠军。后来,在Willow Garage的支持下,它的积极发展得以继续,由Gary Bradsky和Vadim Pisarevsky领导了该项目。OpenCV现在支持与计算机视觉和机器学习有关的多种算法,并且正在日益扩展。
OpenCV支持多种编程语言,例如C++、Python、JAVA等,并且可在windows、linux、OS X、Android和IOS等不同平台上使用。基于CUDA和OpenCL的高速GPU操作的接口也正在积极开发中。
OpenCV-Python是用于OpenCV的Python API,结合了OpenCV C++ API和Python语言的最佳特性。
OpenCV-Python是旨在解决计算机视觉问题的Python专用库。
Python是由Guido van Rossum发起的通用编程语言,很快就非常流行,主要是因为它的简单性和代码可读性。它使程序员可以用较少的代码行表达想法,而不会降低可读性。
与C/C++之类的语言相比,Python速度较慢。也就是说,可以使用C/C++轻松扩展Python,这使我们能够用C/C++编写计算密集型代码并创建可用作Python模块的Python包装器。这给我们带来了两个好处:首先,代码与原始C/C++代码一样快(因为它是在后台运行的实际C++代码),其次,在Python中比C/C++编写代码更容易。OpenCV-Python是原始OpenCV C++实现的Python包装器。
OpenCV-Python利用了Numpy,这是一个高度优化的库,用于使用MATLAB样式的语法进行数值运算。所有OpenCV数组结构都与Numpy数组相互转换。这也使与使用Numpy的其他库(例如SciPy和Matplotlib)的集成变得更加容易。
OpenCV引入了一组新的教程,它们将指导您完成OpenCV-Python中可用的各种功能。本指南主要针对OpenCV 3.x版本(尽管大多数教程也适用于OpenCV 2.x)。
建议先了解Python和Numpy,因为本指南将不介绍它们。要使用OpenCV-Python编写优化的代码,必须先明白Numpy。
本教程最初由Abid Rahman K.在Alexander Mordvintsev的指导下作为google Summer of Code 2013计划的一部分启动。
OpenCV需要您!
由于OpenCV是开放源代码计划,因此欢迎所有人为这个库,文档和教程做出贡献。如果您在本教程中发现任何错误(从小的拼写错误到代码或概念中的严重错误),请随时通过在Github中
:https://github.com/opencv/opencv 克隆OpenCV 并提交请求请求来更正它。OpenCV开发人员将检查您的请求请求,给您重要的反馈,并且(一旦通过审阅者的批准)它将被合并到OpenCV中。然后,您将成为开源贡献者:-)
随着新模块添加到OpenCV-Python中,本教程将不得不进行扩展。如果您熟悉特定的算法,并且可以编写一个包括算法基本理论和显示示例用法的代码的教程,欢迎你这样做。
记住,我们可以共同使这个项目取得巨大成功!
贡献者
以下是向OpenCV-Python提交了教程的贡献者列表。
其他资源
在本教程中我们将学习在你的Fedora系统中设置OpenCV-Python。针对Fedora 18(64位)和Fedora 19(32位)进行以下步骤。
可以通过两种方式在Fedora中安装OpenCV-Python:1)从fedora存储库中可用的预构建二进制文件安装,2)从源代码进行编译。在本节中,我们将同时看到这两种方法。
另一个重要的事情是所需的其他库。OpenCV-Python仅需要Numpy(除了其他依赖关系,我们将在后面看到)。但是在本教程中,我们还使用Matplotlib进行一些简单而又漂亮的作图(与OpenCV相比,感觉好多了)。Matplotlib是可选的,但强烈建议安装。同样,我们还将看到IPython,这是一个强烈推荐的交互式Python终端。
以root用户身份在终端中使用以下命令安装所有软件包。
$ yum install numpy opencv *
打开Python IDLE(或IPython),然后在Python终端中键入以下代码。
>>> import cv2 as cv
>>> print( cv.__version__ )
如果打印出来的结果没有任何错误,那就恭喜!你已经成功安装了OpenCV-Python。
这很简单。但是这里有一个问题。Yum仓库可能不总是包含最新版本的 OpenCV。例如,在撰写本教程时,yum 库包含2.4.5,而最新的 OpenCV 版本是2.4.6。对于 Python API,最新版本总是包含更好的支持。另外,取决于所使用的驱动程序、ffmpeg、gstreamer软件包等,相机支持,视频播放等可能会出现问题。
所以我个人的偏好是下一种方法,即从源代码编译。在某个时候,如果你想为OpenCV 做贡献,你也需要这个。
从源代码编译起初可能看起来有点复杂,但是一旦你成功了,就没有什么复杂的了。
首先,我们将安装一些依赖项。有些是强制性的,有些是可选的。可选的依赖项,如果不需要,可以跳过。
强制依赖
我们需要CMake来配置安装,GCC进行编译,Python-devel和Numpy来创建Python扩展等。
yum install cmake
yum install python-devel numpy
yum install gcc gcc-c++
接下来,我们需要GTK对GUI功能的支持,相机支持(libdc1394,v4l),媒体支持(ffmpeg,gstreamer)等。
yum install gtk2-devel
yum install libdc1394-devel
yum install ffmpeg-devel
yum install gstreamer-plugins-base-devel
可选依赖项
以上依赖关系足以在你的fedora计算机中安装OpenCV。但是根据你的要求,你可能需要一些额外的依赖项。此类可选依赖项的列表如下。你可以跳过或安装它,取决于你:)
OpenCV附带了用于图像格式(例如PNG,JPEG,JPEG2000,TIFF,WebP等)的支持文件。但是它可能有些旧。如果要获取最新的库,可以安装这些格式的开发文件。
yum install libpng-devel
yum install libjpeg-turbo-devel
yum install jasper-devel
yum install openexr-devel
yum install libtiff-devel
yum install libwebp-devel
几个OpenCV功能与英特尔的线程构建模块(TBB)并行。但是,如果要启用它,则需要先安装TBB。(同样在使用CMake配置安装时,请不要忘记设置-D WITH_TBB = ON。下面更多详细信息。)
yum install tbb-devel
OpenCV使用另一个Eigen库来优化数学运算。因此,如果你的系统中装有Eigen,则可以利用它。(同样在使用CMake配置安装时,请不要忘记设置WITH_EIGEN = ON。下面更多详细信息。)
yum install eigen3-devel
如果你要构建文档(是的,你可以使用完整的搜索功能以HTML格式在系统中创建OpenCV完整官方文档的脱机版本,这样,如果有任何问题,你就不必总是访问Inte.NET,而且非常快捷!!!),你需要安装Doxygen(文档生成工具)。
yum install doxygen
接下来,我们必须下载OpenCV。你可以从sourceforge网站:
http://sourceforge.net/projects/opencvlibrary/ 下载最新版本的OpenCV 。然后解压缩文件夹。
或者,你可以从OpenCV的github存储库下载最新的源代码。(如果你想为OpenCV做出贡献,请选择此项。它始终使你的OpenCV保持最新状态)。为此,你需要先安装Git。
yum install git
git clone https://github.com/opencv/opencv.git
它将在主目录(或你指定的目录)中创建一个文件夹OpenCV。克隆可能需要一些时间,具体取决于你的Internet网络。
现在打开一个终端窗口,然后导航到下载的OpenCV文件夹。创建一个新的构建文件夹并导航到它。
mkdir build
cd build
现在,我们已经安装了所有必需的依赖项,让我们安装OpenCV。必须使用CMake配置安装。它指定要安装的模块,安装路径,要使用的其他库,是否要编译的文档和示例等。下面的命令通常用于配置(从build文件夹执行)。
cmake -D CMAKE_BUILD_TYPE = RELEASE -D CMAKE_INSTALL_PREFIX = / usr / local ..
它指定构建类型为“发布模式”,安装路径为/usr/local。在每个选项之前标志-D,在最后观察标志..。简而言之,这是一种格式:
cmake [-D <flag>] [-D <flag>] ..
你可以指定任意数量的标志,但是每个标志前面应带有-D。
因此,在本教程中,我们将安装具有TBB和Eigen支持的OpenCV。我们还构建了文档,但是不包括性能测试和构建示例。我们还会禁用与GPU相关的模块(因为我们使用的是OpenCV-Python,因此我们不需要与GPU相关的模块。这为我们节省了一些时间)。
(以下所有命令都可以在单个cmake语句中完成,但为了便于理解,此处将其拆分。)
...
-- GUI:
-- GTK+ 2.x: YES (ver 2.24.19)
-- GThread : YES (ver 2.36.3)
-- Video I/O:
-- DC1394 2.x: YES (ver 2.2.0)
-- FFMPEG: YES
-- codec: YES (ver 54.92.100)
-- format: YES (ver 54.63.104)
-- util: YES (ver 52.18.100)
-- swscale: YES (ver 2.2.100)
-- gentoo-style: YES
-- GStreamer:
-- base: YES (ver 0.10.36)
-- video: YES (ver 0.10.36)
-- App: YES (ver 0.10.36)
-- riff: YES (ver 0.10.36)
-- pbutils: YES (ver 0.10.36)
-- V4L/V4L2: Using libv4l (ver 1.0.0)
-- Other third-party libraries:
-- Use Eigen: YES (ver 3.1.4)
-- Use TBB: YES (ver 4.0 interface 6004)
-- Python:
-- Interpreter: /usr/bin/python2 (ver 2.7.5)
-- Libraries: /lib/libpython2.7.so (ver 2.7.5)
-- numpy: /usr/lib/python2.7/site-packages/numpy/core/include (ver 1.7.1)
-- packages path: lib/python2.7/site-packages
...
还有许多其他标志和设置。它留给你以作进一步的探索。
现在,你可以使用make命令构建文件,并使用make install命令进行安装。make install应该以root身份执行。
make
su
make install
安装结束。所有文件都安装在/usr/local/文件夹中。但是要使用它,你的Python应该能够找到OpenCV模块。你有两个选择。
要构建文档,只需输入以下命令:
make doxygen
然后打开
opencv/build/doc/doxygen/html/index.html并将其添加到浏览器中。
在本教程中,我们将学习在Ubuntu System中设置OpenCV-Python。以下步骤针对Ubuntu 16.04和18.04(均为64位)进行了测试。
可以通过两种方式在Ubuntu中安装OpenCV-Python:
另一个重要的事情是所需的其他库。OpenCV-Python仅需要Numpy(除了其他依赖关系,我们将在后面看到)。但是在本教程中,我们还使用Matplotlib进行一些简单而又漂亮的绘图目的(与OpenCV相比,我感觉好多了)。Matplotlib是可选的,但强烈建议使用。同样,我们还将看到IPython,这是一个强烈推荐的交互式Python终端。
仅用于编程和开发OpenCV应用程序时,此方法最有效。
在终端(以root用户身份)中使用以下命令安装
python-opencv:https://packages.ubuntu.com/tRusty/python-opencv软件包。
$ sudo apt-get install python-opencv
打开Python IDLE(或IPython),然后在Python终端中键入以下代码。
import cv2 as cv
print(cv.__version__)
如果打印出来的结果没有任何错误,那就恭喜!你已经成功安装了OpenCV-Python。
这看起很容易,但也可能出现问题。Apt存储库不一定总是包含最新版本的OpenCV。例如,在编写本教程时,apt存储库包含2.4.8,而最新的OpenCV版本是3.x。关于Python API,最新版本将始终包含更好的支持和最新的错误修复。
因此,要获取最新的源代码,首选方法是从源代码进行编译。同样在某个时间点,如果你想为OpenCV做出贡献,则将通过这种方式。
首先,从源代码进行编译似乎有些复杂,但是一旦成功完成,就没有什么复杂的了。
首先,我们将安装一些依赖项。有些是必需的,有些是可选的。如果不想,可以跳过可选的依赖项。
所需的构建依赖项
我们需要CMake来配置安装,需要GCC进行编译,需要Python-devel和Numpy来构建Python依赖项等。
sudo apt-get install cmake
sudo apt-get install gcc g++
支持python2:sudo apt-get install python-dev python-numpy
支持python3:sudo apt-get install python3-dev python3-numpy
接下来,我们需要GUI功能的GTK支持,相机支持(v4l),媒体支持(ffmpeg,gstreamer)等。
sudo apt-get install libavcodec-dev libavformat-dev libswscale-dev
sudo apt-get install libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev
支持gtk2:sudo apt-get install libgtk2.0-dev
支持gtk3:sudo apt-get install libgtk-3-dev
可选依赖项
以上依赖关系足以在你的Ubuntu计算机中安装OpenCV。但是根据你的需求,你可能需要一些额外的依赖项。此类可选依赖项的列表如下。你可以跳过或安装它,取决于你:)
OpenCV附带了用于图像格式(例如PNG,JPEG,JPEG2000,TIFF,WebP等)的支持文件。但是它可能有些旧。如果要获取最新的库,可以为这些格式的系统库安装开发文件。
sudo apt-get install libpng-dev
sudo apt-get install libjpeg-dev
sudo apt-get install libopenexr-dev
sudo apt-get install libtiff-dev
sudo apt-get install libwebp-dev
注意
如果你使用的是Ubuntu 16.04,则还可以安装libjasper-dev以添加对JPEG2000格式的系统级别支持。
下载OpenCV
要从OpenCV的GitHub
Repository:https://github.com/opencv/opencv下载最新的源代码。 (如果你想为OpenCV做出贡献,请选择此项。为此,你需要先安装Git)
$ sudo apt-get install git
$ git clone https://github.com/opencv/opencv.git
它将在当前目录中创建一个文件夹"opencv"。下载可能需要一些时间,具体取决于你的Internet网络。
现在打开一个终端窗口,并导航到下载的"opencv"文件夹。创建一个新的"build"文件夹并导航到它。
$ mkdir build
$ cd build
配置和安装
现在我们有了所有必需的依赖项,让我们安装OpenCV。必须使用CMake配置安装。它指定要安装的模块,安装路径,要使用的其他库,是否要编译的文档和示例等。大多数工作都是使用配置良好的默认参数自动完成的。
以下命令通常用于配置OpenCV库构建(从构建文件夹执行):$ cmake ../
OpenCV的默认默认设置为"Release"构建类型,安装路径为/usr/local。有关CMake选项的更多信息,请参考OpenCV C++编译指南
:https://docs.opencv.org/4.1.2/d7/d9f/tutoriallinuxinstall.html
你应该在CMake输出中看到以下几行(它们意味着正确找到了Python):
-- Python 2:
-- Interpreter: /usr/bin/python2.7 (ver 2.7.6)
-- Libraries: /usr/lib/x86_64-linux-gnu/libpython2.7.so (ver 2.7.6)
-- numpy: /usr/lib/python2.7/dist-packages/numpy/core/include (ver 1.8.2)
-- packages path: lib/python2.7/dist-packages
--
-- Python 3:
-- Interpreter: /usr/bin/python3.4 (ver 3.4.3)
-- Libraries: /usr/lib/x86_64-linux-gnu/libpython3.4m.so (ver 3.4.3)
-- numpy: /usr/lib/python3/dist-packages/numpy/core/include (ver 1.8.2)
-- packages path: lib/python3.4/dist-packages
现在,使用make命令构建文件,然后使用make install命令安装文件。
$ make
# sudo make install
安装结束。所有文件都安装在/usr/local/文件夹中。打开终端,然后尝试导入cv2。
import cv2 as cv
print(cv.__version__)
在本教程中,我们将学习在你的Windows系统中设置OpenCV-Python。
下面的步骤在装有Visual Studio 2010和Visual Studio 2012的Windows 7-64位计算机上进行了测试。屏幕截图展示的是VS2012。
>>> import cv2 as cv
>>> print( cv.__version__ )
如果打印出来的结果没有任何错误,那就恭喜!你已经成功安装了OpenCV-Python。
注意
在这种情况下,我们使用的是32位Python软件包二进制文件。但是,如果要将OpenCV用于x64,则将安装Python软件包的64位二进制文件。问题在于,没有Numpy的官方64位二进制文件。你必须自行构建。为此,你必须使用与构建Python相同的编译器。启动Python IDLE时,它会显示编译器详细信息。你可以在此处:
http://stackoverflow.com/q/2676763/1134940 获得更多信息。因此,你的系统必须具有相同的Visual Studio版本并从源代码构建Numpy。
拥有64位Python软件包的另一种方法是使用来自第三方(如Anaconda:
http://www.continuum.io/downloads、 Enthought:https://www.enthought.com/downloads/)等现成Python发行版。它的大小会更大,但可以满足你的所有需求。一切都在一个外壳中。你也可以下载32位版本。
a. 单击Browse Source然后找到opencv文件夹。
b. 单击Browse Build然后找到我们创建的构建文件夹。
c. 点击Configure。
d. 它将打开一个新窗口以选择编译器。选择适当的编译器(此处为Visual Studio 11),然后单击Finish。
e. 等待分析完成。
注意
我们没有安装其他支持如TBB、Eigen、Qt、Documentation等。在这里很难解释清楚。我们将添加更详细的视频,或者你可以随意修改。
如果你有Windows计算机,请从源代码编译OpenCV。做各种各样极客。如果遇到任何问题,请访问OpenCV论坛并描述你的问题。
读取图像
使用cv.imread()函数读取图像。图像应该在工作目录或图像的完整路径应给出。
第二个参数是一个标志,它指定了读取图像的方式。
注意
除了这三个标志,你可以分别简单地传递整数1、0或-1。
请参见下面的代码:
import numpy as np
import cv2 as cv
#加载彩色灰度图像
img = cv.imread('messi5.jpg',0)
注意
即使图像路径错误,它也不会引发任何错误,但是print(img)会给出None
显示图像
使用函数cv.imshow()在窗口中显示图像。窗口自动适合图像尺寸。
第一个参数是窗口名称,它是一个字符串。第二个参数是我们的对象。你可以根据需要创建任意多个窗口,但可以使用不同的窗口名称。
cv.imshow('image',img)
cv.wAItKey(0)
cv.destroyAllWindows()
窗口的屏幕截图如下所示(在Fedora-Gnome机器中):
cv.waitKey()是一个键盘绑定函数。其参数是以毫秒为单位的时间。该函数等待任何键盘事件指定的毫秒。如果您在这段时间内按下任何键,程序将继续运行。如果0被传递,它将无限期地等待一次敲击键。它也可以设置为检测特定的按键,例如,如果按下键 a 等,我们将在下面讨论。
注意
除了键盘绑定事件外,此功能还处理许多其他GUI事件,因此你必须使用它来实际显示图像。
cv.destroyAllWindows()只会破坏我们创建的所有窗口。如果要销毁任何特定的窗口,请使用函数 cv.destroyWindow()在其中传递确切的窗口名称作为参数。
注意
在特殊情况下,你可以创建一个空窗口,然后再将图像加载到该窗口。在这种情况下,你可以指定窗口是否可调整大小。这是通过功能cv.namedWindow()完成的。默认情况下,该标志为cv.WINDOW_AUTOSIZE。但是,如果将标志指定为cv.WINDOW_NORMAL,则可以调整窗口大小。当图像尺寸过大以及向窗口添加跟踪栏时,这将很有帮助。
请参见下面的代码:
cv.namedWindow('image',cv.WINDOW_NORMAL)
cv.imshow('image',img)
cv.waitKey(0)
cv.destroyAllWindows()
写入图像
使用函数cv.imwrite()保存图像。
第一个参数是文件名,第二个参数是要保存的图像。cv.imwrite('messigray.png',img)
这会将图像以PNG格式保存在工作目录中。
总结
在下面的程序中,以灰度加载图像,显示图像,按s保存图像并退出,或者按ESC键直接退出而不保存。
import numpy as np
import cv2 as cv
img = cv.imread('messi5.jpg',0)
cv.imshow('image',img)
k = cv.waitKey(0)
if k == 27: # 等待ESC退出
cv.destroyAllWindows()
elif k == ord('s'): # 等待关键字,保存和退出
cv.imwrite('messigray.png',img)
cv.destroyAllWindows()
注意
如果使用的是64位计算机,则必须k = cv.waitKey(0)按如下所示修改行:k = cv.waitKey(0) & 0xFF
使用Matplotlib
Matplotlib是Python的绘图库,可为你提供多种绘图方法。你将在接下来的文章中看到它们。在这里,你将学习如何使用Matplotlib显示图像。你可以使用Matplotlib缩放图像,保存图像等。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('messi5.jpg',0)
plt.imshow(img, cmap = 'gray', interpolation = 'bicubic')
plt.xticks([]), plt.yticks([]) # 隐藏 x 轴和 y 轴上的刻度值
plt.show()
窗口的屏幕截图如下所示:
还可以看看
Matplotlib中提供了许多绘图选项。请参考Matplotlib文档以获取更多详细信息。
注意
OpenCV加载的彩色图像处于BGR模式。但是Matplotlib以RGB模式显示。因此,如果使用OpenCV读取彩色图像,则Matplotlib中将无法正确显示彩色图像。有关更多详细信息,请参见练习。
其他资源
练习题
通常情况下,我们必须用摄像机捕捉实时画面。提供了一个非常简单的界面。让我们从摄像头捕捉一段视频(我使用的是我笔记本电脑内置的网络摄像头) ,将其转换成灰度视频并显示出来。只是一个简单的任务开始。
要捕获视频,你需要创建一个 VideoCapture 对象。它的参数可以是设备索引或视频文件的名称。设备索引就是指定哪个摄像头的数字。正常情况下,一个摄像头会被连接(就像我的情况一样)。所以我简单地传0(或-1)。你可以通过传递1来选择第二个相机,以此类推。在此之后,你可以逐帧捕获。但是在最后,不要忘记释放俘虏。
import numpy as np
import cv2 as cv
cap = cv.VideoCapture(0)
if not cap.isOpened():
print("Cannot open camera")
exit()
while True:
# 逐帧捕获
ret, frame = cap.read()
# 如果正确读取帧,ret为True
if not ret:
print("Can't receive frame (stream end?). Exiting ...")
break
# 我们在框架上的操作到这里
gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
# 显示结果帧e
cv.imshow('frame', gray)
if cv.waitKey(1) == ord('q'):
break
# 完成所有操作后,释放捕获器
cap.release()
cv.destroyAllWindows()
cap.read()返回布尔值(True/ False)。如果正确读取了帧,它将为True。因此,你可以通过检查此返回值来检查视频的结尾。
有时,cap可能尚未初始化捕获。在这种情况下,此代码显示错误。你可以通过cap.isOpened()方法检查它是否已初始化。如果是True,那么确定。否则,使用cap.open()打开它。
你还可以使用cap.get(propId)方法访问该视频的某些功能,其中propId是0到18之间的一个数字。每个数字表示视频的属性(如果适用于该视频),并且可以显示完整的详细信息在这里看到:cv::VideoCapture::get()。其中一些值可以使用cap.set(propId,value)进行修改。value是你想要的新值。
例如,我可以通过cap.get(cv.CAP_PROP_FRAME_WIDTH)和cap.get(cv.CAP_PROP_FRAME_HEIGHT)检查框架的宽度和高度。默认情况下,它的分辨率为640x480。但我想将其修改为320x240。只需使用和即可。ret = cap.set(cv.CAP_PROP_FRAME_WIDTH,320) and ret = cap.set(cv.CAP_PROP_FRAME_HEIGHT,240).
注意
如果出现错误,请确保使用任何其他相机应用程序(例如Linux中的Cheese)都可以正常使用相机。
它与从相机捕获相同,只是用视频文件名更改摄像机索引。另外,在显示框架时,请使用适当的时间cv.waitKey()。如果太小,则视频将非常快,而如果太大,则视频将变得很慢(嗯,这就是显示慢动作的方式)。正常情况下25毫秒就可以了。
import numpy as np
import cv2 as cv
cap = cv.VideoCapture('vtest.avi')
while cap.isOpened():
ret, frame = cap.read()
# 如果正确读取帧,ret为True
if not ret:
print("Can't receive frame (stream end?). Exiting ...")
break
gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
cv.imshow('frame', gray)
if cv.waitKey(1) == ord('q'):
break
cap.release()
cv.destroyAllWindows()
注意
确保安装了正确的 ffmpeg 或 gstreamer 版本。有时,使用视频捕获(Video Capture)是一件令人头疼的事情,主要原因是错误地安装了 ffmpeg / gstreamer。
所以我们捕捉一个视频,一帧一帧地处理,我们想要保存这个视频。对于图像,它非常简单,只需使用 cv.imwrite()。这里还需要做一些工作。
这次我们创建一个 VideoWriter 对象。我们应该指定输出文件名(例如: output.avi)。然后我们应该指定 FourCC 代码(详见下一段)。然后传递帧率的数量和帧大小。最后一个是颜色标志。如果为 True,编码器期望颜色帧,否则它与灰度帧一起工作。
FourCC:
http://en.wikipedia.org/wiki/FourCC 是用于指定视频编解码器的4字节代码。可用代码列表可在fourcc.org中:http://www.fourcc.org/codecs.php 找到。它取决于平台。遵循编解码器对我来说效果很好。
FourCC代码作为MJPG的cv.VideoWriter_fourcc('M','J','P','G')or cv.VideoWriter_fourcc(*'MJPG')传递。
在从摄像机捕获的代码下面,沿垂直方向翻转每一帧并保存。
import numpy as np
import cv2 as cv
cap = cv.VideoCapture(0)
# 定义编解码器并创建VideoWriter对象
fourcc = cv.VideoWriter_fourcc(*'XVID')
out = cv.VideoWriter('output.avi', fourcc, 20.0, (640, 480))
while cap.isOpened():
ret, frame = cap.read()
if not ret:
print("Can't receive frame (stream end?). Exiting ...")
break
frame = cv.flip(frame, 0)
# 写翻转的框架
out.write(frame)
cv.imshow('frame', frame)
if cv.waitKey(1) == ord('q'):
break
# 完成工作后释放所有内容
cap.release()
out.release()
cv.destroyAllWindows()
在上述所有功能中,您将看到一些常见的参数,如下所示:
画线
要绘制一条线,您需要传递线的开始和结束坐标。我们将创建一个黑色图像,并从左上角到右下角在其上绘制一条蓝线。
import numpy as np
import cv2 as cv
# 创建黑色的图像
img = np.zeros((512,512,3), np.uint8)
# 绘制一条厚度为5的蓝色对角线
cv.line(img,(0,0),(511,511),(255,0,0),5)
画矩形
要绘制矩形,您需要矩形的左上角和右下角。这次,我们将在图像的右上角绘制一个绿色矩形。
cv.rectangle(img,(384,0),(510,128),(0,255,0),3)
画圆圈
要绘制一个圆,需要其中心坐标和半径。我们将在上面绘制的矩形内绘制一个圆。
cv.circle(img,(447,63), 63, (0,0,255), -1)
画椭圆
要绘制椭圆,我们需要传递几个参数。一个参数是中心位置(x,y)。下一个参数是轴长度(长轴长度,短轴长度)。angle是椭圆沿逆时针方向旋转的角度。startAngle和endAngle表示从主轴沿顺时针方向测量的椭圆弧的开始和结束。即给出0和360给出完整的椭圆。有关更多详细信息,请参阅cv.ellipse的文档。下面的示例在图像的中心绘制一个椭圆形。
cv.ellipse(img,(256,256),(100,50),0,0,180,255,-1)
画多边形
要绘制多边形,首先需要顶点的坐标。将这些点组成形状为ROWSx1x2的数组,其中ROWS是顶点数,并且其类型应为int32。在这里,我们绘制了一个带有四个顶点的黄色小多边形。
pts = np.array([[10,5],[20,30],[70,20],[50,10]], np.int32)
pts = pts.reshape((-1,1,2))
cv.polylines(img,[pts],True,(0,255,255))
注意
如果第三个参数为False,您将获得一条连接所有点的折线,而不是闭合形状。
cv.polylines()可用于绘制多条线。只需创建要绘制的所有线条的列表,然后将其传递给函数即可。所有线条将单独绘制。与为每条线调用cv.line相比,绘制一组线是一种更好,更快的方法。
向图像添加文本:
要将文本放入图像中,需要指定以下内容。
我们将在白色图像上写入OpenCV。
font = cv.FONT_HERSHEY_SIMPLEX
cv.putText(img,'OpenCV',(10,500), font, 4,(255,255,255),2,cv.LINE_AA)
现在是时候查看我们绘图的最终结果了。正如您在以前的文章中学习的那样,显示图像以查看它。
在这里,我们创建一个简单的应用程序,无论我们在哪里双击它,都可以在图像上绘制一个圆。
首先,我们创建一个鼠标回调函数,该函数在发生鼠标事件时执行。鼠标事件可以是与鼠标相关的任何事物,例如左键按下,左键按下,左键双击等。它为我们提供了每个鼠标事件的坐标(x,y)。通过此活动和地点,我们可以做任何我们喜欢的事情。要列出所有可用的可用事件,请在Python终端中运行以下代码:
import cv2 as cv
events = [i for i in dir(cv) if 'EVENT' in i]
print( events )
创建鼠标回调函数具有特定的格式,该格式在所有地方都相同。它仅在功能上有所不同。因此,我们的鼠标回调函数可以做一件事,在我们双击的地方绘制一个圆圈。因此,请参见下面的代码。代码在注释中是不言自明的:
import numpy as np
import cv2 as cv
# 鼠标回调函数
def draw_circle(event,x,y,flags,param):
if event == cv.EVENT_LBUTTONDBLCLK:
cv.circle(img,(x,y),100,(255,0,0),-1)
# 创建一个黑色的图像,一个窗口,并绑定到窗口的功能
img = np.zeros((512,512,3), np.uint8)
cv.namedWindow('image')
cv.setMouseCallback('image',draw_circle)
while(1):
cv.imshow('image',img)
if cv.waitKey(20) & 0xFF == 27:
break
cv.destroyAllWindows()
现在我们去寻找一个更好的应用。在这里,我们通过拖动鼠标来绘制矩形或圆形(取决于我们选择的模式) ,就像我们在 Paint 应用程序中所做的那样。所以我们的鼠标回调函数有两部分,一部分用于绘制矩形,另一部分用于绘制圆形。这个具体的例子对于创建和理解一些交互式应用程序非常有帮助,比如目标跟踪,图像分割地图等等。
import numpy as np
import cv2 as cv
drawing = False # 如果按下鼠标,则为真
mode = True # 如果为真,绘制矩形。按 m 键可以切换到曲线
ix,iy = -1,-1
# 鼠标回调函数
def draw_circle(event,x,y,flags,param):
global ix,iy,drawing,mode
if event == cv.EVENT_LBUTTONDOWN:
drawing = True
ix,iy = x,y
elif event == cv.EVENT_MOUSEMOVE:
if drawing == True:
if mode == True:
cv.rectangle(img,(ix,iy),(x,y),(0,255,0),-1)
else:
cv.circle(img,(x,y),5,(0,0,255),-1)
elif event == cv.EVENT_LBUTTONUP:
drawing = False
if mode == True:
cv.rectangle(img,(ix,iy),(x,y),(0,255,0),-1)
else:
cv.circle(img,(x,y),5,(0,0,255),-1)
本节中的几乎所有操作都主要与Numpy相关,而不是与OpenCV相关。要使用OpenCV编写更好的优化代码,需要Numpy的丰富知识。
(由于大多数示例都是单行代码,因此示例在Python终端中显示)
加载彩色图像:
>>> import numpy as np
>>> import cv2 as cv
>>> img = cv.imread('messi5.jpg')
你可以通过行和列坐标来访问像素值。对于 BGR 图像,它返回一个由蓝色、绿色和红色值组成的数组。对于灰度图像,只返回相应的灰度。
>>> px = img[100,100]
>>> print( px )
[157 166 200]
# 仅访问蓝色像素
>>> blue = img[100,100,0]
>>> print( blue )
157
可以用相同的方式修改像素值。
>>> img[100,100] = [255,255,255]
>>> print( img[100,100] )
[255 255 255]
警告
Numpy是用于快速数组计算的优化库。因此,简单地访问每个像素值并对其进行修改将非常缓慢,因此不建议使用。
注意
上面的方法通常用于选择数组的区域,例如前5行和后3列。对于单个像素访问,Numpy数组方法array.item()和array.itemset())被认为更好,但是它们始终返回标量。如果要访问所有B,G,R值,则需要分别调用所有的array.item()。
更好的像素访问和编辑方法:
# 访问 RED 值
>>> img.item(10,10,2)
59
# 修改 RED 值
>>> img.itemset((10,10,2),100)
>>> img.item(10,10,2)
100
图像属性包括行数,列数和通道数,图像数据类型,像素数等。
图像的形状可通过img.shape访问。它返回行,列和通道数的元组(如果图像是彩色的):
>>> print( img.shape )
(342, 548, 3)
注意
如果图像是灰度的,则返回的元组仅包含行数和列数,因此这是检查加载的图像是灰度还是彩色的好方法。
像素总数可通过访问img.size:
>>> print( img.size )
562248
图像数据类型通过img.dtype获得:
>>> print( img.dtype )
uint8
注意
img.dtype在调试时非常重要,因为OpenCV-Python代码中的大量错误是由无效的数据类型引起的。
有时候,你不得不处理一些特定区域的图像。对于图像中的眼睛检测,首先对整个图像进行人脸检测。在获取人脸图像时,我们只选择人脸区域,搜索其中的眼睛,而不是搜索整个图像。它提高了准确性(因为眼睛总是在面部上:D )和性能(因为我们搜索的区域很小)。
使用Numpy索引再次获得ROI。在这里,我要选择球并将其复制到图像中的另一个区域:
>>> ball = img[280:340, 330:390]
>>> img[273:333, 100:160] = ball
检查以下结果:
有时你需要分别处理图像的B,G,R通道。在这种情况下,你需要将BGR图像拆分为单个通道。在其他情况下,你可能需要将这些单独的频道加入BGR图片。你可以通过以下方式简单地做到这一点:
>>> b,g,r = cv.split(img) >>> img = cv.merge((b,g,r))
要么
>>> b = img [:, :, 0]
假设你要将所有红色像素都设置为零,则无需先拆分通道。numpy索引更快:
>>> img [:, :, 2] = 0
警告
cv.split()是一项耗时的操作(就时间而言)。因此,仅在必要时才这样做。否则请进行Numpy索引。
如果要在图像周围创建边框(如相框),则可以使用cv.copyMakeBorder()。但是它在卷积运算,零填充等方面有更多应用。此函数采用以下参数:
下面是一个示例代码,演示了所有这些边框类型,以便更好地理解:
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
BLUE = [255,0,0]
img1 = cv.imread('opencv-logo.png')
replicate = cv.copyMakeBorder(img1,10,10,10,10,cv.BORDER_REPLICATE)
reflect = cv.copyMakeBorder(img1,10,10,10,10,cv.BORDER_REFLECT)
reflect101 = cv.copyMakeBorder(img1,10,10,10,10,cv.BORDER_REFLECT_101)
wrap = cv.copyMakeBorder(img1,10,10,10,10,cv.BORDER_WRAP)
constant= cv.copyMakeBorder(img1,10,10,10,10,cv.BORDER_CONSTANT,value=BLUE)
plt.subplot(231),plt.imshow(img1,'gray'),plt.title('ORIGINAL')
plt.subplot(232),plt.imshow(replicate,'gray'),plt.title('REPLICATE')
plt.subplot(233),plt.imshow(reflect,'gray'),plt.title('REFLECT')
plt.subplot(234),plt.imshow(reflect101,'gray'),plt.title('REFLECT_101')
plt.subplot(235),plt.imshow(wrap,'gray'),plt.title('WRAP')
plt.subplot(236),plt.imshow(constant,'gray'),plt.title('CONSTANT')
plt.show()
请参阅下面的结果。(图像与matplotlib一起显示。因此红色和蓝色通道将互换):
您可以通过OpenCV函数cv.add()或仅通过numpy操作res = img1 + img2添加两个图像。两个图像应具有相同的深度和类型,或者第二个图像可以只是一个标量值。
注意
OpenCV加法和Numpy加法之间有区别。OpenCV加法是饱和运算,而Numpy加法是模运算。
例如,考虑以下示例:
>>> x = np.uint8([250])
>>> y = np.uint8([10])
>>> print( cv.add(x,y) ) # 250+10 = 260 => 255
[[255]]
>>> print( x+y ) # 250+10 = 260 % 256 = 4
[4]
当添加两个图像时,它将更加可见。OpenCV功能将提供更好的结果。因此,始终最好坚持使用OpenCV功能。
这也是图像加法,但是对图像赋予不同的权重,以使其具有融合或透明的感觉。根据以下等式添加图像:
通过从 α 从 0→1 更改,您可以在一个图像到另一个图像之间执行很酷的过渡。
在这里,我拍摄了两个图像,将它们融合在一起。第一幅图像的权重为0.7,第二幅图像的权重为0.3。cv.addWeighted()在图像上应用以下公式。
在这里,γ 被视为零。
img1 = cv.imread('ml.png')
img2 = cv.imread('opencv-logo.png')
dst = cv.addWeighted(img1,0.7,img2,0.3,0)
cv.imshow('dst',dst)
cv.waitKey(0)
cv.destroyAllWindows()
检查以下结果:
这包括按位 AND、 OR、NOT 和 XOR 操作。它们在提取图像的任何部分(我们将在后面的章节中看到)、定义和处理非矩形 ROI 等方面非常有用。 下面我们将看到一个例子,如何改变一个图像的特定区域。我想把 OpenCV 的标志放在一个图像上面。如果我添加两个图像,它会改变颜色。如果我混合它,我得到一个透明的效果。但我希望它是不透明的。如果是一个矩形区域,我可以使用 ROI,就像我们在上一章中所做的那样。但是 OpenCV 的 logo 不是长方形的。所以你可以使用如下的按位操作来实现:
我想在图像上方放置OpenCV徽标。如果添加两个图像,它将改变颜色。如果混合它,我将获得透明效果。但我希望它不透明。如果是矩形区域,则可以像上一章一样使用ROI。但是OpenCV徽标不是矩形。因此,您可以按如下所示进行按位操作:
# 加载两张图片
img1 = cv.imread('messi5.jpg')
img2 = cv.imread('opencv-logo-white.png')
# 我想把logo放在左上角,所以我创建了ROI
rows,cols,channels = img2.shape
roi = img1[0:rows, 0:cols ]
# 现在创建logo的掩码,并同时创建其相反掩码
img2gray = cv.cvtColor(img2,cv.COLOR_BGR2GRAY)
ret, mask = cv.threshold(img2gray, 10, 255, cv.THRESH_BINARY)
mask_inv = cv.bitwise_not(mask)
# 现在将ROI中logo的区域涂黑
img1_bg = cv.bitwise_and(roi,roi,mask = mask_inv)
# 仅从logo图像中提取logo区域
img2_fg = cv.bitwise_and(img2,img2,mask = mask)
# 将logo放入ROI并修改主图像
dst = cv.add(img1_bg,img2_fg)
img1[0:rows, 0:cols ] = dst
cv.imshow('res',img1)
cv.waitKey(0)
cv.destroyAllWindows()
请看下面的结果。左图显示了我们创建的mask。右图显示最终结果。为了更好地理解,显示上面代码中的所有中间映像,特别是 img1bg 和 img2fg。
在图像处理中,由于每秒要处理大量操作,因此必须使代码不仅提供正确的解决方案,而且还必须以最快的方式提供。因此,在本章中,你将学习
除了OpenCV,Python还提供了一个模块time,这有助于衡量执行时间。另一个模块profile有助于获取有关代码的详细报告,例如代码中每个函数花费了多少时间,调用了函数的次数等。但是,如果你使用的是IPython,则所有这些功能都集成在用户友好的界面中方式。我们将看到一些重要的信息,有关更多详细信息,请查看“ 其他资源”部分中的链接。
cv.getTickCount函数返回从参考事件(如打开机器的那一刻)到调用此函数那一刻之间的时钟周期数。因此,如果在函数执行之前和之后调用它,则会获得用于执行函数的时钟周期数。
cv.getTickFrequency函数返回时钟周期的频率或每秒的时钟周期数。因此,要找到执行时间(以秒为单位),你可以执行以下操作:
e1 = cv.getTickCount()
# 你的执行代码
e2 = cv.getTickCount()
time = (e2 - e1)/ cv.getTickFrequency()
我们将通过以下示例进行演示。下面的示例应用中位数过滤,其内核的奇数范围为5到49。(不必担心结果会是什么样,这不是我们的目标):
img1 = cv.imread('messi5.jpg')
e1 = cv.getTickCount()
for i in range(5,49,2):
img1 = cv.medianBlur(img1,i)
e2 = cv.getTickCount()
t = (e2 - e1)/cv.getTickFrequency()
print( t )
# 我得到的结果是0.521107655秒
注意
你可以使用时间模块执行相同的操作。代替cv.getTickCount,使用time.time()函数。然后取两次相差。
许多 OpenCV 函数都是使用 SSE2、 AVX 等进行优化的。 它还包含未优化的代码。因此,如果我们的系统支持这些特性,我们就应该利用它们(几乎所有现代的处理器都支持它们)。在编译时默认启用它。因此,如果启用了 OpenCV,它将运行优化的代码,否则它将运行未优化的代码。你可以使用 cvUseoptimized 检查是否启用 / 禁用和 cvSetuseoptimized 以启用 / 禁用它。让我们看一个简单的例子。
# 检查是否启用了优化
In [5]: cv.useOptimized()
Out[5]: True
In [6]: %timeit res = cv.medianBlur(img,49)
10 loops, best of 3: 34.9 ms per loop
# 关闭它
In [7]: cv.setUseOptimized(False)
In [8]: cv.useOptimized()
Out[8]: False
In [9]: %timeit res = cv.medianBlur(img,49)
10 loops, best of 3: 64.1 ms per loop
看,优化的中值滤波比未优化的版本快2倍。如果你检查其来源,你可以看到中值滤波是 SIMD 优化。因此,你可以使用它在代码顶部启用优化(请记住,它是默认启用的)
有时你可能需要比较两个类似操作的性能。IPython为你提供了一个神奇的命令计时器来执行此操作。它会多次运行代码以获得更准确的结果。同样,它们适用于测量单行代码。
例如,你知道以下哪个加法运算更好,x = 5; y = x**2, x = 5; y = x*x, x = np.uint8([5]); y = x*x或y = np.square(x)?我们将在IPython shell中使用timeit得到答案。
In [10]: x = 5
In [11]: %timeit y=x**2
10000000 loops, best of 3: 73 ns per loop
In [12]: %timeit y=x*x
10000000 loops, best of 3: 58.3 ns per loop
In [15]: z = np.uint8([5])
In [17]: %timeit y=z*z
1000000 loops, best of 3: 1.25 us per loop
In [19]: %timeit y=np.square(z)
1000000 loops, best of 3: 1.16 us per loop
你可以看到x = 5; y = x x最快,比Numpy快20倍左右。如果你还考虑阵列的创建,它可能会快100倍。酷吧?(大量开发人员正在研究此问题)*
注意
Python标量操作比Numpy标量操作快。因此,对于包含一两个元素的运算,Python标量比Numpy数组好。当数组大小稍大时,Numpy会占优势。
我们将再尝试一个示例。这次,我们将比较cv.countNonZero和np.count_nonzero对于同一张图片的性能。
In [35]: %timeit z = cv.countNonZero(img)
100000 loops, best of 3: 15.8 us per loop
In [36]: %timeit z = np.count_nonzero(img)
1000 loops, best of 3: 370 us per loop
看,OpenCV 函数比 Numpy 函数快近25倍。
注意
通常,OpenCV函数比Numpy函数要快。因此,对于相同的操作,首选OpenCV功能。但是,可能会有例外,尤其是当Numpy处理视图而不是副本时。
还有其他一些魔术命令可以用来测量性能,性能分析,行性能分析,内存测量等。它们都有很好的文档记录。因此,此处仅提供指向这些文档的链接。建议有兴趣的读者尝试一下。
有几种技术和编码方法可以充分利用 Python 和 Numpy 的最大性能。这里只注明相关信息,并提供重要信息来源的链接。这里要注意的主要事情是,首先尝试以一种简单的方式实现算法。一旦它运行起来,分析它,找到瓶颈并优化它们。
即使执行了所有这些操作后,如果你的代码仍然很慢,或者不可避免地需要使用大循环,请使用Cython等其他库来使其更快。
OpenCV中有超过150种颜色空间转换方法。但是我们将研究只有两个最广泛使用的,BGR↔灰色和BGR↔HSV。
对于颜色转换,我们使用cv函数。cvtColor(input_image, flag),其中flag决定转换的类型。
对于BGR→灰度转换,我们使用标志cv.COLORBGR2GRAY。类似地,对于BGR→HSV,我们使用标志cv.COLORBGR2HSV。要获取其他标记,只需在Python终端中运行以下命令:
>>> import cv2 as cv
>>> flags = [i for i in dir(cv) if i.startswith('COLOR_')]
>>> print( flags )
注意
HSV的色相范围为[0,179],饱和度范围为[0,255],值范围为[0,255]。不同的软件使用不同的规模。因此,如果你要将OpenCV值和它们比较,你需要将这些范围标准化。
现在我们知道了如何将BGR图像转换成HSV,我们可以使用它来提取一个有颜色的对象。在HSV中比在BGR颜色空间中更容易表示颜色。在我们的应用程序中,我们将尝试提取一个蓝色的对象。方法如下:
下面是详细注释的代码:
import cv2 as cv
import numpy as np
cap = cv.VideoCapture(0)
while(1):
# 读取帧
_, frame = cap.read()
# 转换颜色空间 BGR 到 HSV
hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)
# 定义HSV中蓝色的范围
lower_blue = np.array([110,50,50])
upper_blue = np.array([130,255,255])
# 设置HSV的阈值使得只取蓝色
mask = cv.inRange(hsv, lower_blue, upper_blue)
# 将掩膜和图像逐像素相加
res = cv.bitwise_and(frame,frame, mask= mask)
cv.imshow('frame',frame)
cv.imshow('mask',mask)
cv.imshow('res',res)
k = cv.waitKey(5) & 0xFF
if k == 27:
break
cv.destroyAllWindows()
下图显示了对蓝色对象的跟踪:
注意
图像中有一些噪点。我们将在后面的章节中看到如何删除它们。
这是对象跟踪中最简单的方法。一旦学习了轮廓的功能,你就可以做很多事情,例如找到该对象的质心并使用它来跟踪对象,仅通过将手移到相机前面以及其他许多有趣的东西就可以绘制图表。
这是在stackoverflow.com上发现的一个常见问题。它非常简单,你可以使用相同的函数cv.cvtColor()。你只需传递你想要的BGR值,而不是传递图像。例如,要查找绿色的HSV值,请在Python终端中尝试以下命令:
>>> green = np.uint8([[[0,255,0 ]]])
>>> hsv_green = cv.cvtColor(green,cv.COLOR_BGR2HSV)
>>> print( hsv_green )
[[[ 60 255 255]]]
现在把[H- 10,100,100]和[H+ 10,255, 255]分别作为下界和上界。除了这个方法之外,你可以使用任何图像编辑工具(如GIMP或任何在线转换器)来查找这些值,但是不要忘记调整HSV范围。
练习题
OpenCV提供了两个转换函数cv.warpAffine和cv.warpPerspective,您可以使用它们进行各种转换。cv.warpAffine采用2x3转换矩阵,而cv.warpPerspective采用3x3转换矩阵作为输入。
缩放
缩放只是调整图像的大小。为此,OpenCV带有一个函数cv.resize()。图像的大小可以手动指定,也可以指定缩放比例。也可使用不同的插值方法。首选的插值方法是cv.INTER_AREA用于缩小,cv.INTER_CUBIC(慢)和cv.INTER_LINEAR用于缩放。默认情况下,出于所有调整大小的目的,使用的插值方法为cv.INTER_LINEAR。您可以使用以下方法调整输入图像的大小:
import numpy as np
import cv2 as cv
img = cv.imread('messi5.jpg')
res = cv.resize(img,None,fx=2, fy=2, interpolation = cv.INTER_CUBIC)
#或者
height, width = img.shape[:2]
res = cv.resize(img,(2*width, 2*height), interpolation = cv.INTER_CUBIC)
平移
平移是物体位置的移动。如果您知道在(x,y)方向上的位移,则将其设为(t_x,$ty$),你可以创建转换矩阵M,如下所示:
您可以将其放入np.float32类型的Numpy数组中,并将其传递给cv.warpAffine函数。参见下面偏移为(100, 50)的示例:
import numpy as np
import cv2 as cv
img = cv.imread('messi5.jpg',0)
rows,cols = img.shape
M = np.float32([[1,0,100],[0,1,50]])
dst = cv.warpAffine(img,M,(cols,rows))
cv.imshow('img',dst)
cv.waitKey(0)
cv.destroyAllWindows()
注意
cv.warpAffine函数的第三个参数是输出图像的大小,其形式应为(width,height)。记住width =列数,height =行数。
你将看到下面的结果:
旋转
图像旋转角度为$θ$是通过以下形式的变换矩阵实现的:
但是OpenCV提供了可缩放的旋转以及可调整的旋转中心,因此您可以在自己喜欢的任何位置旋转。修改后的变换矩阵为
其中:
为了找到此转换矩阵,OpenCV提供了一个函数cv.getRotationMatrix2D。请检查以下示例,该示例将图像相对于中心旋转90度而没有任何缩放比例。
img = cv.imread('messi5.jpg',0)
rows,cols = img.shape
# cols-1 和 rows-1 是坐标限制
M = cv.getRotationMatrix2D(((cols-1)/2.0,(rows-1)/2.0),90,1)
dst = cv.warpAffine(img,M,(cols,rows))
查看结果:
仿射变换
在仿射变换中,原始图像中的所有平行线在输出图像中仍将平行。为了找到变换矩阵,我们需要输入图像中的三个点及其在输出图像中的对应位置。然后cv.getAffineTransform将创建一个2x3矩阵,该矩阵将传递给cv.warpAffine。
查看以下示例,并查看我选择的点(以绿色标记):
img = cv.imread('drawing.png')
rows,cols,ch = img.shape
pts1 = np.float32([[50,50],[200,50],[50,200]])
pts2 = np.float32([[10,100],[200,50],[100,250]])
M = cv.getAffineTransform(pts1,pts2)
dst = cv.warpAffine(img,M,(cols,rows))
plt.subplot(121),plt.imshow(img),plt.title('Input')
plt.subplot(122),plt.imshow(dst),plt.title('Output')
查看结果:
透视变换
对于透视变换,您需要3x3变换矩阵。即使在转换后,直线也将保持直线。要找到此变换矩阵,您需要在输入图像上有4个点,在输出图像上需要相应的点。在这四个点中,其中三个不应共线。然后可以通过函数
cv.getPerspectiveTransform找到变换矩阵。然后将cv.warpPerspective应用于此3x3转换矩阵。
请参见下面的代码:
img = cv.imread('sudoku.png')
rows,cols,ch = img.shape
pts1 = np.float32([[56,65],[368,52],[28,387],[389,390]])
pts2 = np.float32([[0,0],[300,0],[0,300],[300,300]])
M = cv.getPerspectiveTransform(pts1,pts2)
dst = cv.warpPerspective(img,M,(300,300))
plt.subplot(121),plt.imshow(img),plt.title('Input')
plt.subplot(122),plt.imshow(dst),plt.title('Output')
plt.show()
结果:
在这里,问题直截了当。对于每个像素,应用相同的阈值。如果像素值小于阈值,则将其设置为0,否则将其设置为最大值。函数cv.threshold用于应用阈值。第一个参数是源图像,它应该是灰度图像。第二个参数是阈值,用于对像素值进行分类。第三个参数是分配给超过阈值的像素值的最大值。OpenCV提供了不同类型的阈值,这由函数的第四个参数给出。通过使用cv.THRESH_BINARY类型。所有简单的阈值类型为:
该方法返回两个输出。第一个是使用的阈值,第二个输出是阈值后的图像。
此代码比较了不同的简单阈值类型:
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('gradient.png',0)
ret,thresh1 = cv.threshold(img,127,255,cv.THRESH_BINARY)
ret,thresh2 = cv.threshold(img,127,255,cv.THRESH_BINARY_INV)
ret,thresh3 = cv.threshold(img,127,255,cv.THRESH_TRUNC)
ret,thresh4 = cv.threshold(img,127,255,cv.THRESH_TOZERO)
ret,thresh5 = cv.threshold(img,127,255,cv.THRESH_TOZERO_INV)
titles = ['Original Image','BINARY','BINARY_INV','TRUNC','TOZERO','TOZERO_INV']
images = [img, thresh1, thresh2, thresh3, thresh4, thresh5]
for i in xrange(6):
plt.subplot(2,3,i+1),plt.imshow(images[i],'gray')
plt.title(titles[i])
plt.xticks([]),plt.yticks([])
plt.show()
注意
为了绘制多个图像,我们使用plt.subplot()函数。请查看matplotlib文档以获取更多详细信息。
该代码产生以下结果:
在上一节中,我们使用一个全局值作为阈值。但这可能并非在所有情况下都很好,例如,如果图像在不同区域具有不同的光照条件。在这种情况下,自适应阈值阈值化可以提供帮助。在此,算法基于像素周围的小区域确定像素的阈值。因此,对于同一图像的不同区域,我们获得了不同的阈值,这为光照度变化的图像提供了更好的结果。
除上述参数外,方法cv.adaptiveThreshold还包含三个输入参数:
adaptiveMethod决定阈值是如何计算的:
cv.ADAPTIVETHRESHMEAN_C::阈值是邻近区域的平均值减去常数C。
cv.ADAPTIVETHRESHGAUSSIAN_C:阈值是邻域值的高斯加权总和减去常数C。
该BLOCKSIZE确定附近区域的大小,C是从邻域像素的平均或加权总和中减去的一个常数。
下面的代码比较了光照变化的图像的全局阈值和自适应阈值:
结果:
在全局阈值化中,我们使用任意选择的值作为阈值。相反,Otsu的方法避免了必须选择一个值并自动确定它的情况。
考虑仅具有两个不同图像值的图像(双峰图像),其中直方图将仅包含两个峰。一个好的阈值应该在这两个值的中间。类似地,Otsu的方法从图像直方图中确定最佳全局阈值。
为此,使用了cv.threshold作为附加标志传递。阈值可以任意选择。然后,算法找到最佳阈值,该阈值作为第一输出返回。
查看以下示例。输入图像为噪点图像。在第一种情况下,采用值为127的全局阈值。在第二种情况下,直接采用Otsu阈值法。在第三种情况下,首先使用5x5高斯核对图像进行滤波以去除噪声,然后应用Otsu阈值处理。了解噪声滤波如何改善结果。
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('noisy2.png',0)
# 全局阈值
ret1,th1 = cv.threshold(img,127,255,cv.THRESH_BINARY)
# Otsu阈值
ret2,th2 = cv.threshold(img,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
# 高斯滤波后再采用Otsu阈值
blur = cv.GaussianBlur(img,(5,5),0)
ret3,th3 = cv.threshold(blur,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
# 绘制所有图像及其直方图
images = [img, 0, th1,
img, 0, th2,
blur, 0, th3]
titles = ['Original Noisy Image','Histogram','Global Thresholding (v=127)',
'Original Noisy Image','Histogram',"Otsu's Thresholding",
'Gaussian filtered Image','Histogram',"Otsu's Thresholding"]
for i in xrange(3):
plt.subplot(3,3,i*3+1),plt.imshow(images[i*3],'gray')
plt.title(titles[i*3]), plt.xticks([]), plt.yticks([])
plt.subplot(3,3,i*3+2),plt.hist(images[i*3].ravel(),256)
plt.title(titles[i*3+1]), plt.xticks([]), plt.yticks([])
plt.subplot(3,3,i*3+3),plt.imshow(images[i*3+2],'gray')
plt.title(titles[i*3+2]), plt.xticks([]), plt.yticks([])
plt.show()
结果:
本节演示了Otsu二值化的Python实现,以展示其实际工作方式。如果您不感兴趣,可以跳过此步骤。
由于我们正在处理双峰图像,因此Otsu的算法尝试找到一个阈值(t),该阈值将由关系式给出的加权类内方差最小化:
其中
实际上,它找到位于两个峰值之间的t值,以使两个类别的差异最小。它可以简单地在Python中实现,如下所示:
img = cv.imread('noisy2.png',0)
blur = cv.GaussianBlur(img,(5,5),0)
# 寻找归一化直方图和对应的累积分布函数
hist = cv.calcHist([blur],[0],None,[256],[0,256])
hist_norm = hist.ravel()/hist.max()
Q = hist_norm.cumsum()
bins = np.arange(256)
fn_min = np.inf
thresh = -1
for i in xrange(1,256):
p1,p2 = np.hsplit(hist_norm,[i]) # 概率
q1,q2 = Q[i],Q[255]-Q[i] # 对类求和
b1,b2 = np.hsplit(bins,[i]) # 权重
# 寻找均值和方差
m1,m2 = np.sum(p1*b1)/q1, np.sum(p2*b2)/q2
v1,v2 = np.sum(((b1-m1)**2)*p1)/q1,np.sum(((b2-m2)**2)*p2)/q2
# 计算最小化函数
fn = v1*q1 + v2*q2
if fn < fn_min:
fn_min = fn
thresh = i
# 使用OpenCV函数找到otsu的阈值
ret, otsu = cv.threshold(blur,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
print( "{} {}".format(thresh,ret) )
学会:
与一维信号一样,还可以使用各种低通滤波器(LPF),高通滤波器(HPF)等对图像进行滤波。LPF有助于消除噪声,使图像模糊等。HPF滤波器有助于在图像中找到边缘。
OpenCV提供了一个函数cv.filter2D来将内核与图像进行卷积。例如,我们将尝试对图像进行平均滤波。5x5平均滤波器内核如下所示:
操作如下:保持这个内核在一个像素上,将所有低于这个内核的25个像素相加,取其平均值,然后用新的平均值替换中心像素。它将对图像中的所有像素继续此操作。试试这个代码,并检查结果:
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('opencv_logo.png')
kernel = np.ones((5,5),np.float32)/25
dst = cv.filter2D(img,-1,kernel)
plt.subplot(121),plt.imshow(img),plt.title('Original')
plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(dst),plt.title('Averaging')
plt.xticks([]), plt.yticks([])
plt.show()
结果:
通过将图像与低通滤波器内核进行卷积来实现图像模糊。这对于消除噪音很有用。它实际上从图像中消除了高频部分(例如噪声,边缘)。因此,在此操作中边缘有些模糊。(有一些模糊技术也可以不模糊边缘)。OpenCV主要提供四种类型的模糊技术。
1.平均
这是通过将图像与归一化框滤镜进行卷积来完成的。它仅获取内核区域下所有像素的平均值,并替换中心元素。这是通过功能cv.blur()或cv.boxFilter()完成的。检查文档以获取有关内核的更多详细信息。我们应该指定内核的宽度和高度。3x3归一化框式过滤器如下所示:
注意
如果您不想使用标准化的框式过滤器,请使用cv.boxFilter()。将参数normalize = False传递给函数。
查看下面的示例演示,其内核大小为5x5:
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('opencv-logo-white.png')
blur = cv.blur(img,(5,5))
plt.subplot(121),plt.imshow(img),plt.title('Original')
plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(blur),plt.title('Blurred')
plt.xticks([]), plt.yticks([])
plt.show()
结果:
2.高斯模糊
在这种情况下,代替盒式滤波器,使用了高斯核。这是通过功能cv.GaussianBlur() 完成的。我们应指定内核的宽度和高度,该宽度和高度应为正数和奇数。我们还应指定X和Y方向的标准偏差,分别为sigmaX和sigmaY。如果仅指定sigmaX,则将sigmaY与sigmaX相同。如果两个都为零,则根据内核大小进行计算。高斯模糊对于从图像中去除高斯噪声非常有效。
如果需要,可以使用函数cv.getGaussianKernel() 创建高斯内核。
可以修改以上代码以实现高斯模糊:
blur = cv.GaussianBlur(img,(5,5),0)
结果:
3.中位模糊
在这里,函数cv.medianBlur() 提取内核区域下所有像素的中值,并将中心元素替换为该中值。这对于消除图像中的椒盐噪声非常有效。有趣的是,在上述过滤器中,中心元素是新计算的值,该值可以是图像中的像素值或新值。但是在中值模糊中,中心元素总是被图像中的某些像素值代替。有效降低噪音。其内核大小应为正奇数整数。
在此演示中,我向原始图像添加了50%的噪声并应用了中值模糊。检查结果:
median = cv.medianBlur(img,5)
结果:
4.双边滤波
cv.bilateralFilter() 在去除噪声的同时保持边缘清晰锐利非常有效。但是,与其他过滤器相比,该操作速度较慢。我们已经看到,高斯滤波器采用像素周围的邻域并找到其高斯加权平均值。高斯滤波器仅是空间的函数,也就是说,滤波时会考虑附近的像素。它不考虑像素是否具有几乎相同的强度。它不考虑像素是否是边缘像素。因此它也模糊了边缘,这是我们不想做的。
双边滤波器在空间中也采用高斯滤波器,但是又有一个高斯滤波器,它是像素差的函数。空间的高斯函数确保仅考虑附近像素的模糊,而强度差的高斯函数确保仅考虑强度与中心像素相似的那些像素的模糊。由于边缘的像素强度变化较大,因此可以保留边缘。
以下示例显示了使用双边过滤器(有关参数的详细信息,请访问docs)。
blur = cv.bilateralFilter(img,9,75,75)
结果:
看到,表面上的纹理消失了,但是边缘仍然保留。
在这一章当中, 我们将学习不同的形态学操作,例如侵蚀,膨胀,开运算,闭运算等。 我们将看到不同的功能,例如:cv.erode(),cv.dilate(), cv.morphologyEx()等。
形态变换是一些基于图像形状的简单操作。通常在二进制图像上执行。它需要两个输入,一个是我们的原始图像,第二个是决定操作性质的结构元素或内核。两种基本的形态学算子是侵蚀和膨胀。然后,它的变体形式(如“打开”,“关闭”,“渐变”等)也开始起作用。在下图的帮助下,我们将一一看到它们:
侵蚀的基本思想就像土壤侵蚀一样,它侵蚀前景物体的边界(尽量使前景保持白色)。它是做什么的呢?内核滑动通过图像(在2D卷积中)。原始图像中的一个像素(无论是1还是0)只有当内核下的所有像素都是1时才被认为是1,否则它就会被侵蚀(变成0)。
结果是,根据内核的大小,边界附近的所有像素都会被丢弃。因此,前景物体的厚度或大小减小,或只是图像中的白色区域减小。它有助于去除小的白色噪声(正如我们在颜色空间章节中看到的),分离两个连接的对象等。
在这里,作为一个例子,我将使用一个5x5内核,它包含了所有的1。让我们看看它是如何工作的:
import cv2 as cv
import numpy as np
img = cv.imread('j.png',0)
kernel = np.ones((5,5),np.uint8)
erosion = cv.erode(img,kernel,iterations = 1)
结果:
它与侵蚀正好相反。如果内核下的至少一个像素为“ 1”,则像素元素为“ 1”。因此,它会增加图像中的白色区域或增加前景对象的大小。通常,在消除噪音的情况下,腐蚀后会膨胀。因为腐蚀会消除白噪声,但也会缩小物体。因此,我们对其进行了扩展。由于噪音消失了,它们不会回来,但是我们的目标区域增加了。在连接对象的损坏部分时也很有用。
dilation = cv.dilate(img,kernel,iterations = 1)
结果:
开放只是侵蚀然后扩张的另一个名称。如上文所述,它对于消除噪音很有用。在这里,我们使用函数cv.morphologyEx()
opening = cv.morphologyEx(img, cv.MORPH_OPEN, kernel)
结果:
闭运算与开运算相反,先扩张然后再侵蚀。在关闭前景对象内部的小孔或对象上的小黑点时很有用。
closing = cv.morphologyEx(img, cv.MORPH_CLOSE, kernel)
这是图像扩张和侵蚀之间的区别。
结果将看起来像对象的轮廓。
gradient = cv.morphologyEx(img, cv.MORPH_GRADIENT, kernel)
它是输入图像和图像开运算之差。下面的示例针对9x9内核完成。
tophat = cv.morphologyEx(img, cv.MORPH_TOPHAT, kernel)
结果:
这是输入图像和图像闭运算之差。
blackhat = cv.morphologyEx(img, cv.MORPH_BLACKHAT, kernel)
结果:
在Numpy的帮助下,我们在前面的示例中手动创建了一个结构元素。它是矩形。但是在某些情况下,您可能需要椭圆形/圆形的内核。因此,为此,OpenCV具有一个函数cv.getStructuringElement()。您只需传递内核的形状和大小,即可获得所需的内核。
# 矩形内核
>>> cv.getStructuringElement(cv.MORPH_RECT,(5,5))
array([[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]], dtype=uint8)
# 椭圆内核
>>> cv.getStructuringElement(cv.MORPH_ELLIPSE,(5,5))
array([[0, 0, 1, 0, 0],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[0, 0, 1, 0, 0]], dtype=uint8)
# 十字内核
>>> cv.getStructuringElement(cv.MORPH_CROSS,(5,5))
array([[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[1, 1, 1, 1, 1],
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0]], dtype=uint8)
在本章中,我们将学习:
OpenCV提供三种类型的梯度滤波器或高通滤波器,即Sobel,Scharr和Laplacian。我们将看到他们每一种。
1. Sobel 和 Scharr 算子
Sobel算子是高斯平滑加微分运算的联合运算,因此它更抗噪声。逆可以指定要采用的导数方向,垂直或水平(分别通过参数yorder和xorder)。逆还可以通过参数ksize指定内核的大小。如果ksize = -1,则使用3x3 Scharr滤波器,比3x3 Sobel滤波器具有更好的结果。请参阅文档以了解所使用的内核。
2. Laplacian 算子
它计算了由关系$Delta src = frac{partial ^2{src}}{partial x^2} + frac{partial ^2{src}}{partial y^2}$给出的图像的拉普拉斯图,它是每一阶导数通过Sobel算子计算。如果ksize = 1,然后使用以下内核用于过滤:
下面的代码显示了单个图表中的所有算子。所有内核都是5x5大小。输出图像的深度通过-1得到结果的np.uint8型。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('dave.jpg',0)
laplacian = cv.Laplacian(img,cv.CV_64F)
sobelx = cv.Sobel(img,cv.CV_64F,1,0,ksize=5)
sobely = cv.Sobel(img,cv.CV_64F,0,1,ksize=5)
plt.subplot(2,2,1),plt.imshow(img,cmap = 'gray')
plt.title('Original'), plt.xticks([]), plt.yticks([])
plt.subplot(2,2,2),plt.imshow(laplacian,cmap = 'gray')
plt.title('Laplacian'), plt.xticks([]), plt.yticks([])
plt.subplot(2,2,3),plt.imshow(sobelx,cmap = 'gray')
plt.title('Sobel X'), plt.xticks([]), plt.yticks([])
plt.subplot(2,2,4),plt.imshow(sobely,cmap = 'gray')
plt.title('Sobel Y'), plt.xticks([]), plt.yticks([])
plt.show()
结果:
在我们的最后一个示例中,输出数据类型为cv.CV_8U或np.uint8。但这有一个小问题。黑色到白色的过渡被视为正斜率(具有正值),而白色到黑色的过渡被视为负斜率(具有负值)。因此,当您将数据转换为np.uint8时,所有负斜率均设为零。简而言之,您会错过这一边缘信息。
如果要检测两个边缘,更好的选择是将输出数据类型保留为更高的形式,例如cv.CV_16S,cv.CV_64F等,取其绝对值,然后转换回cv.CV_8U。下面的代码演示了用于水平Sobel滤波器和结果差异的此过程。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('box.png',0)
# Output dtype = cv.CV_8U
sobelx8u = cv.Sobel(img,cv.CV_8U,1,0,ksize=5)
# Output dtype = cv.CV_64F. Then take its absolute and convert to cv.CV_8U
sobelx64f = cv.Sobel(img,cv.CV_64F,1,0,ksize=5)
abs_sobel64f = np.absolute(sobelx64f)
sobel_8u = np.uint8(abs_sobel64f)
plt.subplot(1,3,1),plt.imshow(img,cmap = 'gray')
plt.title('Original'), plt.xticks([]), plt.yticks([])
plt.subplot(1,3,2),plt.imshow(sobelx8u,cmap = 'gray')
plt.title('Sobel CV_8U'), plt.xticks([]), plt.yticks([])
plt.subplot(1,3,3),plt.imshow(sobel_8u,cmap = 'gray')
plt.title('Sobel abs(CV_64F)'), plt.xticks([]), plt.yticks([])
plt.show()
结果:
在本章中,我们将学习
Canny Edge Detection是一种流行的边缘检测算法。它由John F. Canny发明
这是一个多阶段算法,我们将经历每个阶段。
降噪
由于边缘检测容易受到图像中噪声的影响,因此第一步是使用5x5高斯滤波器消除图像中的噪声。我们已经在前面的章节中看到了这一点。
查找图像的强度梯度
然后使用Sobel核在水平和垂直方向上对平滑的图像进行滤波,以在水平方向(Gx)和垂直方向(Gy)上获得一阶导数。从这两张图片中,我们可以找到每个像素的边缘渐变和方向,如下所示:
渐变方向始终垂直于边缘。将其舍入为代表垂直,水平和两个对角线方向的四个角度之一。
非极大值抑制
在获得梯度大小和方向后,将对图像进行全面扫描,以去除可能不构成边缘的所有不需要的像素。为此,在每个像素处,检查像素是否是其在梯度方向上附近的局部最大值。查看下面的图片:
点A在边缘(垂直方向)上。渐变方向垂直于边缘。点B和C在梯度方向上。因此,将A点与B点和C点进行检查,看是否形成局部最大值。如果是这样,则考虑将其用于下一阶段,否则将其抑制(置为零)。 简而言之,你得到的结果是带有“细边”的二进制图像。
磁滞阈值
该阶段确定哪些边缘全部是真正的边缘,哪些不是。为此,我们需要两个阈值minVal和maxVal。强度梯度大于maxVal的任何边缘必定是边缘,而小于minVal的那些边缘必定是非边缘,因此将其丢弃。介于这两个阈值之间的对象根据其连通性被分类为边缘或非边缘。如果将它们连接到“边缘”像素,则将它们视为边缘的一部分。否则,它们也将被丢弃。见下图:
边缘A在maxVal之上,因此被视为“确定边缘”。尽管边C低于maxVal,但它连接到边A,因此也被视为有效边,我们得到了完整的曲线。但是边缘B尽管在minVal之上并且与边缘C处于同一区域,但是它没有连接到任何“确保边缘”,因此被丢弃。因此,非常重要的一点是我们必须相应地选择minVal和maxVal以获得正确的结果。
在边缘为长线的假设下,该阶段还消除了小像素噪声。
因此,我们最终得到的是图像中的强边缘。
OpenCV将以上所有内容放在单个函数cv.Canny()中。我们将看到如何使用它。第一个参数是我们的输入图像。第二个和第三个参数分别是我们的minVal和maxVal。第三个参数是perture_size。它是用于查找图像渐变的Sobel内核的大小。默认情况下为3。最后一个参数是L2gradient,它指定用于查找梯度幅度的方程式。如果为True,则使用上面提到的更精确的公式,否则使用以下函数:$Edge_Gradient ; (G) = |Gx| + |Gy|$。默认情况下,它为False。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('messi5.jpg',0)
edges = cv.Canny(img,100,200)
plt.subplot(121),plt.imshow(img,cmap = 'gray')
plt.title('Original Image'), plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(edges,cmap = 'gray')
plt.title('Edge Image'), plt.xticks([]), plt.yticks([])
plt.show()
在本章中,
通常,我们过去使用的是恒定大小的图像。但是在某些情况下,我们需要使用不同分辨率的(相同)图像。例如,当在图像中搜索某些东西(例如人脸)时,我们不确定对象将以多大的尺寸显示在图像中。在这种情况下,我们将需要创建一组具有不同分辨率的相同图像,并在所有图像中搜索对象。这些具有不同分辨率的图像集称为“图像金字塔”(因为当它们堆叠在底部时,最高分辨率的图像位于顶部,最低分辨率的图像位于顶部时,看起来像金字塔)。
有两种图像金字塔:1)高斯金字塔和2)拉普拉斯金字塔
高斯金字塔中的较高级别(低分辨率)是通过删除较低级别(较高分辨率)图像中的连续行和列而形成的。然后,较高级别的每个像素由基础级别的5个像素的贡献与高斯权重形成。通过这样做,$M×N$图像变成M/2 × N/2图像。因此面积减少到原始面积的四分之一。它称为Octave。当我们在金字塔中越靠上时(即分辨率下降),这种模式就会继续。同样,在扩展时,每个级别的面积变为4倍。我们可以使用cv.pyrDown()和cv.pyrUp()函数找到高斯金字塔。
img = cv.imread('messi5.jpg')
lower_reso = cv.pyrDown(higher_reso)
以下是图像金字塔中的4个级别。
现在,您可以使用cv.pyrUp()函数查看图像金字塔。
higher_reso2 = cv.pyrUp(lower_reso)
记住,higherreso2不等于higherreso,因为一旦降低了分辨率,就会丢失信息。下面的图像是3层的金字塔从最小的图像在前面的情况下创建。与原图对比:
拉普拉斯金字塔由高斯金字塔形成。没有专用功能。拉普拉斯金字塔图像仅像边缘图像。它的大多数元素为零。它们用于图像压缩。拉普拉斯金字塔的层由高斯金字塔的层与高斯金字塔的高层的扩展版本之间的差形成。拉普拉斯等级的三个等级如下所示(调整对比度以增强内容):
金字塔的一种应用是图像融合。例如,在图像拼接中,您需要将两个图像堆叠在一起,但是由于图像之间的不连续性,可能看起来不太好。在这种情况下,使用金字塔混合图像可以无缝混合,而不会在图像中保留大量数据。一个经典的例子是将两种水果,橙和苹果混合在一起。现在查看结果本身,以了解我在说什么:
请检查其他资源中的第一个参考,它具有图像混合,拉普拉斯金字塔等的完整图解详细信息。只需完成以下步骤即可:
下面是完整的代码。(为简单起见,每个步骤都是单独进行的,这可能会占用更多的内存。如果需要,可以对其进行优化)。
import cv2 as cv
import numpy as np,sys
A = cv.imread('apple.jpg')
B = cv.imread('orange.jpg')
# 生成A的高斯金字塔
G = A.copy()
gpA = [G]
for i in xrange(6):
G = cv.pyrDown(G)
gpA.append(G)
# 生成B的高斯金字塔
G = B.copy()
gpB = [G]
for i in xrange(6):
G = cv.pyrDown(G)
gpB.append(G)
# 生成A的拉普拉斯金字塔
lpA = [gpA[5]]
for i in xrange(5,0,-1):
GE = cv.pyrUp(gpA[i])
L = cv.subtract(gpA[i-1],GE)
lpA.append(L)
# 生成B的拉普拉斯金字塔
lpB = [gpB[5]]
for i in xrange(5,0,-1):
GE = cv.pyrUp(gpB[i])
L = cv.subtract(gpB[i-1],GE)
lpB.append(L)
# 现在在每个级别中添加左右两半图像
LS = []
for la,lb in zip(lpA,lpB):
rows,cols,dpt = la.shape
ls = np.hstack((la[:,0:cols/2], lb[:,cols/2:]))
LS.append(ls)
# 现在重建
ls_ = LS[0]
for i in xrange(1,6):
ls_ = cv.pyrUp(ls_)
ls_ = cv.add(ls_, LS[i])
# 图像与直接连接的每一半
real = np.hstack((A[:,:cols/2],B[:,cols/2:]))
cv.imwrite('Pyramid_blending2.jpg',ls_)
cv.imwrite('Direct_blending.jpg',real)
##
轮廓可以简单地解释为连接具有相同颜色或强度的所有连续点(沿边界)的曲线。轮廓是用于形状分析以及对象检测和识别的有用工具。
让我们看看如何找到二进制图像的轮廓:
import numpy as np
import cv2 as cv
im = cv.imread('test.jpg')
imgray = cv.cvtColor(im, cv.COLOR_BGR2GRAY)
ret, thresh = cv.threshold(imgray, 127, 255, 0)
contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
findcontour()函数中有三个参数,第一个是源图像,第二个是轮廓检索模式,第三个是轮廓逼近方法。输出等高线和层次结构。轮廓是图像中所有轮廓的Python列表。每个单独的轮廓是一个(x,y)坐标的Numpy数组的边界点的对象。
注意
稍后我们将详细讨论第二和第三个参数以及有关层次结构。在此之前,代码示例中赋予它们的值将适用于所有图像。
要绘制轮廓,请使用cv.drawContours函数。只要有边界点,它也可以用来绘制任何形状。它的第一个参数是源图像,第二个参数是应该作为Python列表传递的轮廓,第三个参数是轮廓的索引(在绘制单个轮廓时有用。要绘制所有轮廓,请传递-1),其余参数是颜色,厚度等等
cv.drawContours(img, contours, -1, (0,255,0), 3)
cv.drawContours(img, contours, 3, (0,255,0), 3)
cnt = contours[4]
cv.drawContours(img, [cnt], 0, (0,255,0), 3)
注意
最后两种方法相似,但是前进时,您会发现最后一种更有用。
这是cv.findContours函数中的第三个参数。它实际上表示什么?
上面我们告诉我们轮廓是强度相同的形状的边界。它存储形状边界的(x,y)坐标。但是它存储所有坐标吗?这是通过这种轮廓近似方法指定的。
如果传递cv.CHAINAPPROXNONE,则将存储所有边界点。但是实际上我们需要所有这些要点吗?例如,您找到了一条直线的轮廓。您是否需要线上的所有点来代表该线?不,我们只需要该线的两个端点即可。这就是cv.CHAINAPPROXSIMPLE所做的。它删除所有冗余点并压缩轮廓,从而节省内存。
下面的矩形图像演示了此技术。只需在轮廓数组中的所有坐标上绘制一个圆(以蓝色绘制)。第一幅图像显示了我用cv.CHAINAPPROXNONE获得的积分(734个点),第二幅图像显示了我用cv.CHAINAPPROXSIMPLE获得的效果(只有4个点)。看,它可以节省多少内存!!!
在本文中,我们将学习
特征矩可以帮助您计算一些特征,例如物体的质心,物体的面积等。请查看特征矩上的维基百科页面。函数cv.moments()提供了所有计算出的矩值的字典。见下文:
import numpy as np
import cv2 as cv
img = cv.imread('star.jpg',0)
ret,thresh = cv.threshold(img,127,255,0)
contours,hierarchy = cv.findContours(thresh, 1, 2)
cnt = contours[0]
M = cv.moments(cnt)
print( M )
从这一刻起,您可以提取有用的数据,例如面积,质心等。质心由关系给出,$Cx = frac{M{10}}{M{00}}$ 和 $Cy = frac{M{01}}{M{00}}$。可以按照以下步骤进行:
cx = int(M['m10']/M['m00'])
cy = int(M['m01']/M['m00'])
轮廓区域由函数cv.contourArea()或从矩M['m00']中给出。
area = cv.contourArea(cnt)
也称为弧长。可以使用cv.arcLength()函数找到它。第二个参数指定形状是闭合轮廓(True)还是曲线。
perimeter = cv.arcLength(cnt,True)
根据我们指定的精度,它可以将轮廓形状近似为顶点数量较少的其他形状。它是Douglas-Peucker算法的实现。检查维基百科页面上的算法和演示。
为了理解这一点,假设您试图在图像中找到一个正方形,但是由于图像中的某些问题,您没有得到一个完美的正方形,而是一个“坏形状”(如下图所示)。现在,您可以使用此功能来近似形状。在这种情况下,第二个参数称为epsilon,它是从轮廓到近似轮廓的最大距离。它是一个精度参数。需要正确选择epsilon才能获得正确的输出。
epsilon = 0.1*cv.arcLength(cnt,True)
approx = cv.approxPolyDP(cnt,epsilon,True)
下面,在第二张图片中,绿线显示了ε=弧长的10%时的近似曲线。第三幅图显示了ε=弧长度的1%时的情况。第三个参数指定曲线是否闭合。
凸包外观看起来与轮廓逼近相似,但不相似(在某些情况下两者可能提供相同的结果)。在这里,cv.convexHull()函数检查曲线是否存在凸凹缺陷并对其进行校正。一般而言,凸曲线是始终凸出或至少平坦的曲线。如果在内部凸出,则称为凸度缺陷。例如,检查下面的手的图像。红线显示手的凸包。双向箭头标记显示凸度缺陷,这是凸包与轮廓线之间的局部最大偏差。
关于它的语法,有一些需要讨论:
hull = cv.convexHull(points[, hull[, clockwise[, returnPoints]]
参数详细信息:
因此,要获得如上图所示的凸包,以下内容就足够了:
hull = cv.convexHull(cnt)
但是,如果要查找凸度缺陷,则需要传递returnPoints = False。为了理解它,我们将拍摄上面的矩形图像。首先,我发现它的轮廓为cnt。现在,我发现它的带有returnPoints = True的凸包,得到以下值:[[[234 202]],[[51 202]],[[51 79]],[[234 79]]],它们是四个角 矩形的点。现在,如果对returnPoints = False执行相同的操作,则会得到以下结果:[[129],[67],[0],[142]]。这些是轮廓中相应点的索引。例如,检查第一个值:cnt [129] = [[234,202]]与第一个结果相同(对于其他结果依此类推)。
当我们讨论凸度缺陷时,您将再次看到它。
cv.isContourConvex()具有检查曲线是否凸出的功能。它只是返回True还是False。没什么大不了的。
k = cv.isContourConvex(cnt)
有两种类型的边界矩形。
它是一个矩形,不考虑物体的旋转。所以边界矩形的面积不是最小的。它是由函数cv.boundingRect()找到的。
令(x,y)为矩形的左上角坐标,而(w,h)为矩形的宽度和高度。
x,y,w,h = cv.boundingRect(cnt)
cv.rectangle(img,(x,y),(x+w,y+h),(0,255,0),2)
这里,边界矩形是用最小面积绘制的,所以它也考虑了旋转。使用的函数是cv.minAreaRect()。它返回一个Box2D结构,其中包含以下细节 -(中心(x,y),(宽度,高度),旋转角度)。但要画出这个矩形,我们需要矩形的四个角。它由函数cv.boxPoints()获得
rect = cv.minAreaRect(cnt)
box = cv.boxPoints(rect)
box = np.int0(box)
cv.drawContours(img,[box],0,(0,0,255),2)
两个矩形都显示在一张单独的图像中。绿色矩形显示正常的边界矩形。红色矩形是旋转后的矩形。
接下来,使用函数**cv.minEnclosingCircle(*()查找对象的圆周。它是一个以最小面积完全覆盖物体的圆。
(x,y),radius = cv.minEnclosingCircle(cnt)
center = (int(x),int(y))
radius = int(radius)
cv.circle(img,center,radius,(0,255,0),2)
下一个是把一个椭圆拟合到一个物体上。它返回内接椭圆的旋转矩形。
ellipse = cv.fitEllipse(cnt)
cv.ellipse(img,ellipse,(0,255,0),2)
同样,我们可以将一条直线拟合到一组点。下图包含一组白点。我们可以近似一条直线。
rows,cols = img.shape[:2]
[vx,vy,x,y] = cv.fitLine(cnt, cv.DIST_L2,0,0.01,0.01)
lefty = int((-x*vy/vx) + y)
righty = int(((cols-x)*vy/vx)+y)
cv.line(img,(cols-1,righty),(0,lefty),(0,255,0),2)
在这里,我们将学习提取一些常用的物体属性,如坚实度,等效直径,掩模图像,平均强度等。更多的功能可以在Matlab regionprops文档中找到。
(注:质心、面积、周长等也属于这一类,但我们在上一章已经见过)
它是对象边界矩形的宽度与高度的比值。
x,y,w,h = cv.boundingRect(cnt)
aspect_ratio = float(w)/h
范围是轮廓区域与边界矩形区域的比值。
area = cv.contourArea(cnt)
x,y,w,h = cv.boundingRect(cnt)
rect_area = w*h
extent = float(area)/rect_area
坚实度是等高线面积与其凸包面积之比。
area = cv.contourArea(cnt)
hull = cv.convexHull(cnt)
hull_area = cv.contourArea(hull)
solidity = float(area)/hull_area
等效直径是面积与轮廓面积相同的圆的直径。
area = cv.contourArea(cnt)
equi_diameter = np.sqrt(4*area/np.pi)
取向是物体指向的角度。以下方法还给出了主轴和副轴的长度。
(x,y),(MA,ma),angle = cv.fitEllipse(cnt)
在某些情况下,我们可能需要构成该对象的所有点。可以按照以下步骤完成:
mask = np.zeros(imgray.shape,np.uint8)
cv.drawContours(mask,[cnt],0,255,-1)
pixelpoints = np.transpose(np.nonzero(mask))
#pixelpoints = cv.findNonZero(mask)
这里提供了两个方法,一个使用Numpy函数,另一个使用OpenCV函数(最后的注释行)。结果也是一样的,只是略有不同。Numpy给出的坐标是(行、列)格式,而OpenCV给出的坐标是(x,y)格式。所以基本上答案是可以互换的。注意,row = x, column = y。
我们可以使用掩码图像找到这些参数。
min_val, max_val, min_loc, max_loc = cv.minMaxLoc(imgray,mask = mask)
在这里,我们可以找到对象的平均颜色。或者可以是灰度模式下物体的平均强度。我们再次使用相同的掩码进行此操作。
mean_val = cv.mean(im,mask = mask)
极点是指对象的最顶部,最底部,最右侧和最左侧的点。
leftmost = tuple(cnt[cnt[:,:,0].argmin()][0])
rightmost = tuple(cnt[cnt[:,:,0].argmax()][0])
topmost = tuple(cnt[cnt[:,:,1].argmin()][0])
bottommost = tuple(cnt[cnt[:,:,1].argmax()][0])
例如,如果我将其应用于印度地图,则会得到以下结果:
在本章中,我们将学习
我们看到了关于轮廓的第二章的凸包。从这个凸包上的任何偏差都可以被认为是凸性缺陷。OpenCV有一个函数来找到这个,cv.convexityDefects()。一个基本的函数调用如下:
hull = cv.convexHull(cnt,returnPoints = False)
defects = cv.convexityDefects(cnt,hull)
注意
记住,我们必须在发现凸包时,传递returnPoints= False,以找到凸性缺陷。
它返回一个数组,其中每行包含这些值—[起点、终点、最远点、到最远点的近似距离]。我们可以用图像把它形象化。我们画一条连接起点和终点的线,然后在最远处画一个圆。记住,返回的前三个值是cnt的索引。所以我们必须从cnt中获取这些值。
import cv2 as cv
import numpy as np
img = cv.imread('star.jpg')
img_gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
ret,thresh = cv.threshold(img_gray, 127, 255,0)
contours,hierarchy = cv.findContours(thresh,2,1)
cnt = contours[0]
hull = cv.convexHull(cnt,returnPoints = False)
defects = cv.convexityDefects(cnt,hull)
for i in range(defects.shape[0]):
s,e,f,d = defects[i,0]
start = tuple(cnt[s][0])
end = tuple(cnt[e][0])
far = tuple(cnt[f][0])
cv.line(img,start,end,[0,255,0],2)
cv.circle(img,far,5,[0,0,255],-1)
cv.imshow('img',img)
cv.waitKey(0)
cv.destroyAllWindows()
查看结果:
这个函数找出图像中一点到轮廓线的最短距离。它返回的距离,点在轮廓线外时为负,点在轮廓线内时为正,点在轮廓线上时为零。
例如,我们可以检查点(50,50)如下:
dist = cv.pointPolygonTest(cnt,(50,50),True)
在函数中,第三个参数是measuredist。如果它是真的,它会找到有符号的距离。如果为假,则查找该点是在轮廓线内部还是外部(分别返回+1、-1和0)。
注意
如果您不想找到距离,请确保第三个参数为False,因为这是一个耗时的过程。因此,将其设置为False可使速度提高2-3倍。
OpenCV附带一个函数cv.matchShapes(),该函数使我们能够比较两个形状或两个轮廓,并返回一个显示相似性的度量。结果越低,匹配越好。它是根据矩值计算出来的。不同的测量方法在文档中有解释。
import cv2 as cv
import numpy as np
img1 = cv.imread('star.jpg',0)
img2 = cv.imread('star2.jpg',0)
ret, thresh = cv.threshold(img1, 127, 255,0)
ret, thresh2 = cv.threshold(img2, 127, 255,0)
contours,hierarchy = cv.findContours(thresh,2,1)
cnt1 = contours[0]
contours,hierarchy = cv.findContours(thresh2,2,1)
cnt2 = contours[0]
ret = cv.matchShapes(cnt1,cnt2,1,0.0)
print( ret )
我尝试过匹配下面给出的不同形状的形状:
我得到以下结果:
看,即使是图像旋转也不会对这个比较产生很大的影响。
参考Hu矩是平移、旋转和比例不变的七个矩。第七个是无偏斜量。这些值可以使用cpu.HuMoments()函数找到。
这次我们学习轮廓的层次,即轮廓中的父子关系。
在前几篇关于轮廓的文章中,我们已经讨论了与OpenCV提供的轮廓相关的几个函数。但是当我们使用cv.findcontour()函数在图像中找到轮廓时,我们已经传递了一个参数,轮廓检索模式。我们通常通过了cv.RETR_LIST或cv.RETR_TREE,效果很好。但这到底意味着什么呢?
另外,在输出中,我们得到了三个数组,第一个是图像,第二个是轮廓,还有一个我们命名为hierarchy的输出(请检查前面文章中的代码)。但我们从未在任何地方使用过这种层次结构。那么这个层级是什么?它是用来做什么的?它与前面提到的函数参数有什么关系?
这就是我们在本文中要讨论的内容。
通常我们使用cv.findcontour()函数来检测图像中的对象,对吧?有时对象在不同的位置。但在某些情况下,某些形状在其他形状中。就像嵌套的图形一样。在这种情况下,我们把外部的称为父类,把内部的称为子类。这样,图像中的轮廓就有了一定的相互关系。我们可以指定一个轮廓是如何相互连接的,比如,它是另一个轮廓的子轮廓,还是父轮廓等等。这种关系的表示称为层次结构。
下面是一个例子:
在这张图中,有一些形状我已经从0-5开始编号。2和2a表示最外层盒子的外部和内部轮廓。
这里,等高线0,1,2在外部或最外面。我们可以说,它们在层级-0中,或者简单地说,它们在同一个层级中。
其次是contour-2a。它可以被认为是contour-2的子级(或者反过来,contour-2是contour-2a的父级)。假设它在层级-1中。类似地,contour-3是contour-2的子级,它位于下一个层次结构中。最后,轮廓4,5是contour-3a的子级,他们在最后一个层级。从对方框的编号来看,我认为contour-4是contour-3a的第一个子级(它也可以是contour-5)。
我提到这些是为了理解一些术语,比如相同层级,外部轮廓,子轮廓,父轮廓,第一个子轮廓等等。现在让我们进入OpenCV。
所以每个轮廓都有它自己的信息关于它是什么层次,谁是它的孩子,谁是它的父母等等。OpenCV将它表示为一个包含四个值的数组:[Next, Previous, First_Child, Parent]
“Next表示同一层次的下一个轮廓。”
例如,在我们的图片中取contour-0。谁是下一个同级别的等高线?这是contour-1。简单地令Next = 1。类似地,Contour-1也是contour-2。所以Next = 2。contour-2呢?同一水平线上没有下一条等高线。简单地,让Next = -1。contour-4呢?它与contour-5处于同一级别。它的下一条等高线是contour-5,所以next = 5。
“Previous表示同一层次上的先前轮廓。”
和上面一样。contour-1之前的等值线为同级别的contour-0。类似地,contour-2也是contour-1。对于contour-0,没有前项,所以设为-1。
“First_Child表示它的第一个子轮廓。”
没有必要作任何解释。对于contour-2, child是contour-2a。从而得到contour-2a对应的指标值。contour-3a呢?它有两个孩子。但我们只关注第一个孩子。它是contour-4。那么First_Child = 4 对contour-3a而言。
“Parent表示其父轮廓的索引。”
它与First_Child相反。对于轮廓线-4和轮廓线-5,父轮廓线都是轮廓线-3a。对于轮廓3a,它是轮廓-3,以此类推。
注意
如果没有子元素或父元素,则该字段被视为-1
现在我们已经了解了OpenCV中使用的层次样式,我们可以借助上面给出的相同图像来检查OpenCV中的轮廓检索模式。一些标志如 cv.RETR_LIST, cv.RETR_TREE,cv.RETR_CCOMP, cv.RETR_EXTERNAL等等的含义。
这是四个标志中最简单的一个(从解释的角度来看)。它只是检索所有的轮廓,但不创建任何亲子关系。在这个规则下,父轮廓和子轮廓是平等的,他们只是轮廓。他们都属于同一层级。
这里,第3和第4项总是-1。但是很明显,下一项和上一项都有对应的值。你自己检查一下就可以了。
下面是我得到的结果,每一行是对应轮廓的层次细节。例如,第一行对应于轮廓0。下一条轮廓是轮廓1。所以Next = 1。没有先前的轮廓,所以Previous=-1。剩下的两个,如前所述,是-1。
>>> hierarchy
array([[[ 1, -1, -1, -1],
[ 2, 0, -1, -1],
[ 3, 1, -1, -1],
[ 4, 2, -1, -1],
[ 5, 3, -1, -1],
[ 6, 4, -1, -1],
[ 7, 5, -1, -1],
[-1, 6, -1, -1]]])
如果您没有使用任何层次结构特性,那么这是在您的代码中使用的最佳选择。
如果使用此标志,它只返回极端外部标志。所有孩子的轮廓都被留下了。我们可以说,根据这项规则,每个家庭只有长子得到关注。它不关心家庭的其他成员:)。
所以在我们的图像中,有多少个极端的外轮廓?在等级0级?有3个,即等值线是0 1 2,对吧?现在试着用这个标志找出等高线。这里,给每个元素的值与上面相同。并与上述结果进行了比较。以下是我得到的:
>>> hierarchy
array([[[ 1, -1, -1, -1],
[ 2, 0, -1, -1],
[-1, 1, -1, -1]]])
如果只想提取外部轮廓,可以使用此标志。它在某些情况下可能有用。
此标志检索所有轮廓并将其排列为2级层次结构。物体的外部轮廓(即物体的边界)放在层次结构-1中。对象内部孔洞的轮廓(如果有)放在层次结构-2中。如果其中有任何对象,则其轮廓仅在层次结构1中重新放置。以及它在层级2中的漏洞等等。
只需考虑在黑色背景上的“白色的零”图像。零的外圆属于第一级,零的内圆属于第二级。
我们可以用一个简单的图像来解释它。这里我用红色标注了等高线的顺序和它们所属的层次,用绿色标注(1或2),顺序与OpenCV检测等高线的顺序相同。
考虑第一个轮廓,即contour-0。这是hierarchy-1。它有两个孔,分别是等高线1和2,属于第二级。因此,对于轮廓-0,在同一层次的下一个轮廓是轮廓-3。previous也没有。在hierarchy-2中,它的第一个子结点是contour-1。它没有父类,因为它在hierarchy-1中。所以它的层次数组是[3,-1,1,-1]
现在contour-1。它在层级-2中。相同层次结构中的下一个(在contour-1的父母关系下)是contour-2。没有previous。没有child,但是parent是contour-0。所以数组是[2,-1,-1,0]
类似的contour-2:它在hierarchy-2中。在contour-0下,同一层次结构中没有下一个轮廓。所以没有Next。previous是contour-1。没有child,parent是contour0。所以数组是[-1,1,-1,0]
contour-3:层次-1的下一个是轮廓-5。以前是contour-0。child是contour4,没有parent。所以数组是[5,0,4,-1]
contour-4:它在contour-3下的层次结构2中,它没有兄弟姐妹。没有next,没有previous,没有child,parent是contour-3。所以数组是[-1,-1,-1,3]
剩下的你可以补充。这是我得到的最终答案:
>>> hierarchy
array([[[ 3, -1, 1, -1],
[ 2, -1, -1, 0],
[-1, 1, -1, 0],
[ 5, 0, 4, -1],
[-1, -1, -1, 3],
[ 7, 3, 6, -1],
[-1, -1, -1, 5],
[ 8, 5, -1, -1],
[-1, 7, -1, -1]]])
这是最后一个家伙,完美先生。它检索所有的轮廓并创建一个完整的家族层次结构列表。它甚至告诉,谁是爷爷,父亲,儿子,孙子,甚至更多…:)。
例如,我拿上面的图片,重写了cv的代码。RETR_TREE,根据OpenCV给出的结果重新排序等高线并进行分析。同样,红色的字母表示轮廓数,绿色的字母表示层次顺序。
取contour-0:它在hierarchy-0中。同一层次结构的next轮廓是轮廓-7。没有previous的轮廓。child是contour-1,没有parent。所以数组是[7,-1,1,-1]
以contour-2为例:它在hierarchy-1中。没有轮廓在同一水平。没有previous。child是contour-3。父母是contour-1。所以数组是[-1,-1,3,1]
剩下的,你自己试试。以下是完整答案:
>>> hierarchy
array([[[ 7, -1, 1, -1],
[-1, -1, 2, 0],
[-1, -1, 3, 1],
[-1, -1, 4, 2],
[-1, -1, 5, 3],
[ 6, -1, -1, 4],
[-1, 5, -1, 4],
[ 8, 0, -1, -1],
[-1, 7, -1, -1]]])
学会
那么直方图是什么?您可以将直方图视为图形或绘图,从而可以总体了解图像的强度分布。它是在X轴上具有像素值(不总是从0到255的范围),在Y轴上具有图像中相应像素数的图。
这只是理解图像的另一种方式。通过查看图像的直方图,您可以直观地了解该图像的对比度,亮度,强度分布等。当今几乎所有图像处理工具都提供直方图功能。以下是剑桥彩色网站的图片,我建议您访问该网站以获取更多详细信息。
您可以看到图像及其直方图。(请记住,此直方图是针对灰度图像而非彩色图像绘制的)。直方图的左侧区域显示图像中较暗像素的数量,而右侧区域则显示明亮像素的数量。从直方图中,您可以看到暗区域多于亮区域,而中间调的数量(中间值的像素值,例如127附近)则非常少。
现在我们有了一个关于直方图的想法,我们可以研究如何找到它。OpenCV和Numpy都为此内置了功能。在使用这些功能之前,我们需要了解一些与直方图有关的术语。
BINS:上面的直方图显示每个像素值的像素数,即从0到255。即,您需要256个值来显示上面的直方图。但是考虑一下,如果您不需要分别找到所有像素值的像素数,而是找到像素值间隔中的像素数怎么办?例如,您需要找到介于0到15之间的像素数,然后找到16到31之间,...,240到255之间的像素数。只需要16个值即可表示直方图。这就是在OpenCV教程中有关直方图的示例中显示的内容。
因此,您要做的就是将整个直方图分成16个子部分,每个子部分的值就是其中所有像素数的总和。每个子部分都称为“ BIN”。在第一种情况下,bin的数量为256个(每个像素一个),而在第二种情况下,bin的数量仅为16个。BINS由OpenCV文档中的histSize术语表示。
DIMS:这是我们为其收集数据的参数的数量。在这种情况下,我们仅收集关于强度值的一件事的数据。所以这里是1。
RANGE:这是您要测量的强度值的范围。通常,它是[0,256],即所有强度值。
1. OpenCV中的直方图计算
因此,现在我们使用cv.calcHist()函数查找直方图。让我们熟悉一下该函数及其参数:
cv.calcHist(images,channels,mask,histSize,ranges [,hist [,accumulate]])
因此,让我们从示例图像开始。只需以灰度模式加载图像并找到其完整直方图即可。
img = cv.imread('home.jpg',0)
hist = cv.calcHist([img],[0],None,[256],[0,256])
hist是256x1的数组,每个值对应于该图像中具有相应像素值的像素数。
2. numpy的直方图计算
Numpy还为您提供了一个函数np.histogram()。因此,除了calcHist()函数外,您可以尝试下面的代码:
hist,bins = np.histogram(img.ravel(),256,[0,256])
hist与我们之前计算的相同。但是bin将具有257个元素,因为Numpy计算出bin的范围为0-0.99、1-1.99、2-2.99等。因此最终范围为255-255.99。为了表示这一点,他们还在最后添加了256。但是我们不需要256。最多255就足够了。
注意
OpenCV函数比np.histogram()快大约40倍。因此,尽可能使用OpenCV函数。
现在我们应该绘制直方图,但是怎么绘制?
有两种方法,
1. 使用Matplotlib
Matplotlib带有直方图绘图功能:matplotlib.pyplot.hist()它直接找到直方图并将其绘制。您无需使用calcHist()或np.histogram()函数来查找直方图。请参见下面的代码:
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('home.jpg',0)
plt.hist(img.ravel(),256,[0,256]); plt.show()
你将得到如下的结果:
或者,您可以使用matplotlib的法线图,这对于BGR图是很好的。为此,您需要首先找到直方图数据。试试下面的代码:
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('home.jpg')
color = ('b','g','r')
for i,col in enumerate(color):
histr = cv.calcHist([img],[i],None,[256],[0,256])
plt.plot(histr,color = col)
plt.xlim([0,256])
plt.show()
结果:
您可以从上图中得出,蓝色在图像中具有一些高值域(显然这应该是由于天空)
2. 使用 OpenCV
好吧,在这里您可以调整直方图的值及其bin值,使其看起来像x,y坐标,以便您可以使用cv.line()或cv.polyline()函数绘制它以生成与上述相同的图像。OpenCV-Python2官方示例已经提供了此功能。检查示例/python/hist.py中的代码。
我们使用了cv.calcHist()来查找整个图像的直方图。如果你想找到图像某些区域的直方图呢?只需创建一个掩码图像,在你要找到直方图为白色,否则黑色。然后把这个作为掩码传递。
img = cv.imread('home.jpg',0)
# create a mask
mask = np.zeros(img.shape[:2], np.uint8)
mask[100:300, 100:400] = 255
masked_img = cv.bitwise_and(img,img,mask = mask)
# 计算掩码区域和非掩码区域的直方图
# 检查作为掩码的第三个参数
hist_full = cv.calcHist([img],[0],None,[256],[0,256])
hist_mask = cv.calcHist([img],[0],mask,[256],[0,256])
plt.subplot(221), plt.imshow(img, 'gray')
plt.subplot(222), plt.imshow(mask,'gray')
plt.subplot(223), plt.imshow(masked_img, 'gray')
plt.subplot(224), plt.plot(hist_full), plt.plot(hist_mask)
plt.xlim([0,256])
plt.show()
查看结果。在直方图中,蓝线表示完整图像的直方图,绿线表示掩码区域的直方图。
在本节中,我们将学习直方图均衡化的概念,并利用它来提高图像的对比度。
考虑这样一个图像,它的像素值仅局限于某个特定的值范围。例如,较亮的图像将把所有像素限制在高值上。但是一幅好的图像会有来自图像所有区域的像素。因此,您需要将这个直方图拉伸到两端(如下图所示,来自wikipedia),这就是直方图均衡化的作用(简单来说)。这通常会提高图像的对比度。
我建议您阅读直方图均衡化上的Wikipedia页面,以获取有关它的更多详细信息。它很好地解释了示例,使您在阅读完之后几乎可以理解所有内容。相反,在这里我们将看到其Numpy实现。之后,我们将看到OpenCV功能。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('wiki.jpg',0)
hist,bins = np.histogram(img.flatten(),256,[0,256])
cdf = hist.cumsum()
cdf_normalized = cdf * float(hist.max()) / cdf.max()
plt.plot(cdf_normalized, color = 'b')
plt.hist(img.flatten(),256,[0,256], color = 'r')
plt.xlim([0,256])
plt.legend(('cdf','histogram'), loc = 'upper left')
plt.show()
你可以看到直方图位于较亮的区域。我们需要全频谱。为此,我们需要一个转换函数,将亮区域的输入像素映射到整个区域的输出像素。这就是直方图均衡化的作用。
现在我们找到最小的直方图值(不包括0),并应用wiki页面中给出的直方图均衡化方程。但我在这里用过,来自Numpy的掩码数组概念数组。对于掩码数组,所有操作都在非掩码元素上执行。您可以从Numpy文档中了解更多关于掩码数组的信息。
cdf_m = np.ma.masked_equal(cdf,0)
cdf_m = (cdf_m - cdf_m.min())*255/(cdf_m.max()-cdf_m.min())
cdf = np.ma.filled(cdf_m,0).astype('uint8')
现在我们有了查找表,该表为我们提供了有关每个输入像素值的输出像素值是什么的信息。因此,我们仅应用变换。
img2 = cdf[img]
现在,我们像以前一样计算其直方图和cdf(您这样做),结果如下所示:
另一个重要的特征是,即使图像是一个较暗的图像(而不是我们使用的一个较亮的图像),经过均衡后,我们将得到几乎相同的图像。因此,这是作为一个“参考工具”,使所有的图像具有相同的照明条件。这在很多情况下都很有用。例如,在人脸识别中,在对人脸数据进行训练之前,对人脸图像进行直方图均衡化处理,使其具有相同的光照条件。
OpenCV具有执行此操作的功能cv.equalizeHist()。它的输入只是灰度图像,输出是我们的直方图均衡图像。 下面是一个简单的代码片段,显示了它与我们使用的同一图像的用法:
img = cv.imread('wiki.jpg',0)
equ = cv.equalizeHist(img)
res = np.hstack((img,equ)) #stacking images side-by-side
cv.imwrite('res.png',res)
因此,现在您可以在不同的光照条件下拍摄不同的图像,对其进行均衡并检查结果。
当图像的直方图限制在特定区域时,直方图均衡化效果很好。在直方图覆盖较大区域(即同时存在亮像素和暗像素)的强度变化较大的地方,效果不好。请检查其他资源中的SOF链接。
我们刚刚看到的第一个直方图均衡化考虑了图像的整体对比度。在许多情况下,这不是一个好主意。例如,下图显示了输入图像及其在全局直方图均衡后的结果。
直方图均衡后,背景对比度确实得到了改善。但是在两个图像中比较雕像的脸。由于亮度过高,我们在那里丢失了大多数信息。这是因为它的直方图不像我们在前面的案例中所看到的那样局限于特定区域(尝试绘制输入图像的直方图,您将获得更多的直觉)。
因此,为了解决这个问题,使用了自适应直方图均衡。在这种情况下,图像被分成称为“tiles”的小块(在OpenCV中,tileSize默认为8x8)。然后,像往常一样对这些块中的每一个进行直方图均衡。因此,在较小的区域中,直方图将限制在一个较小的区域中(除非存在噪声)。如果有噪音,它将被放大。为了避免这种情况,应用了对比度限制。如果任何直方图bin超出指定的对比度限制(在OpenCV中默认为40),则在应用直方图均衡之前,将这些像素裁剪并均匀地分布到其他bin。均衡后,要消除图块边界中的伪影,请应用双线性插值。
下面的代码片段显示了如何在OpenCV中应用CLAHE:
import numpy as np
import cv2 as cv
img = cv.imread('tsukuba_l.png',0)
# create a CLAHE object (Arguments are optional).
clahe = cv.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
cl1 = clahe.apply(img)
cv.imwrite('clahe_2.jpg',cl1)
查看下面的结果,并将其与上面的结果进行比较,尤其是雕像区域:
有关对比度调整的问题:
在本章中,我们将学习查找和绘制2D直方图。这将在以后的章节中有所帮助。
在第一篇文章中,我们计算并绘制了一维直方图。 之所以称为一维,是因为我们仅考虑一个特征,即像素的灰度强度值。 但是在二维直方图中,您要考虑两个特征。 通常,它用于查找颜色直方图,其中两个特征是每个像素的色相和饱和度值。
已经有一个python示例(samples / python / color_histogram.py)用于查找颜色直方图。 我们将尝试了解如何创建这种颜色直方图,这对于理解诸如直方图反向投影之类的更多主题将很有用。
它非常简单,并且使用相同的函数cv.calcHist()进行计算。 对于颜色直方图,我们需要将图像从BGR转换为HSV。(请记住,对于一维直方图,我们从BGR转换为灰度)。对于二维直方图,其参数将进行如下修改:
现在检查以下代码:
import numpy as np
import cv2 as cv
img = cv.imread('home.jpg')
hsv = cv.cvtColor(img,cv.COLOR_BGR2HSV)
hist = cv.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])
就是这样。
Numpy还为此提供了一个特定的函数:np.histogram2d()。(记住,对于一维直方图我们使用了np.histogram())。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('home.jpg')
hsv = cv.cvtColor(img,cv.COLOR_BGR2HSV)
hist, xbins, ybins = np.histogram2d(h.ravel(),s.ravel(),[180,256],[[0,180],[0,256]])
第一个参数是H平面,第二个是S平面,第三个是每个箱子的数量,第四个是它们的范围。
现在我们可以检查如何绘制这个颜色直方图。
方法1:使用 cv.imshow()
我们得到的结果是尺寸为80x256的二维数组。因此,可以使用cv.imshow()函数像平常一样显示它们。它将是一幅灰度图像,除非您知道不同颜色的色相值,否则不会对其中的颜色有太多了解。
方法2:使用Matplotlib
我们可以使用matplotlib.pyplot.imshow()函数绘制具有不同颜色图的2D直方图。它使我们对不同的像素密度有了更好的了解。但是,除非您知道不同颜色的色相值,否则乍一看并不能使我们知道到底是什么颜色。我还是更喜欢这种方法。它简单而更好。
注意
使用此功能时,请记住,插值法应采用最近邻以获得更好的结果。
考虑下面的代码:
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('home.jpg')
hsv = cv.cvtColor(img,cv.COLOR_BGR2HSV)
hist = cv.calcHist( [hsv], [0, 1], None, [180, 256], [0, 180, 0, 256] )
plt.imshow(hist,interpolation = 'nearest')
plt.show()
下面是输入图像及其颜色直方图。X轴显示S值,Y轴显示色相。
在直方图中,您可以在H = 100和S = 200附近看到一些较高的值。它对应于天空的蓝色。同样,在H = 25和S = 100附近可以看到另一个峰值。它对应于宫殿的黄色。您可以使用GIMP等任何图像编辑工具进行验证。
方法3:OpenCV示例样式
OpenCV-Python2示例中有一个颜色直方图的示例代码(samples / python / color_histogram.py)。如果运行代码,则可以看到直方图也显示了相应的颜色。或者简单地,它输出颜色编码的直方图。其结果非常好(尽管您需要添加额外的线束)。
在该代码中,作者在HSV中创建了一个颜色图。然后将其转换为BGR。将所得的直方图图像与此颜色图相乘。他还使用一些预处理步骤来删除小的孤立像素,从而获得良好的直方图。
我将其留给读者来运行代码,对其进行分析并拥有自己的解决方法。下面是与上面相同的图像的代码输出:
您可以在直方图中清楚地看到存在什么颜色,那里是蓝色,那里是黄色,并且由于棋盘的存在而有些白色。很好!
在本章中,我们将学习直方图反投影。
这是由Michael J. Swain和Dana H. Ballard在他们的论文《通过颜色直方图索引》中提出的。
用简单的话说是什么意思?它用于图像分割或在图像中查找感兴趣的对象。简而言之,它创建的图像大小与输入图像相同(但只有一个通道),其中每个像素对应于该像素属于我们物体的概率。用更简单的话来说,与其余部分相比,输出图像将在可能有对象的区域具有更多的白色值。好吧,这是一个直观的解释。(我无法使其更简单)。直方图反投影与camshift算法等配合使用。
我们该怎么做呢?我们创建一个图像的直方图,其中包含我们感兴趣的对象(在我们的示例中是背景,离开播放器等)。对象应尽可能填充图像以获得更好的效果。而且颜色直方图比灰度直方图更可取,因为对象的颜色对比灰度强度是定义对象的好方法。然后,我们将该直方图“反投影”到需要找到对象的测试图像上,换句话说,我们计算出属于背景的每个像素的概率并将其显示出来。在适当的阈值下产生的输出使我们仅获得背景。
1.首先,我们需要计算我们要查找的对象(使其为“ M”)和要搜索的图像(使其为“ I”)的颜色直方图。
import numpy as np
import cv2 as cvfrom matplotlib import pyplot as plt
#roi是我们需要找到的对象或对象区域
roi = cv.imread('rose_red.png')
hsv = cv.cvtColor(roi,cv.COLOR_BGR2HSV)
#目标是我们搜索的图像
target = cv.imread('rose.png')
hsvt = cv.cvtColor(target,cv.COLOR_BGR2HSV)
# 使用calcHist查找直方图。也可以使用np.histogram2d完成
M = cv.calcHist([hsv],[0, 1], None, [180, 256], [0, 180, 0, 256] )
I = cv.calcHist([hsvt],[0, 1], None, [180, 256], [0, 180, 0, 256] )
2.求出比值R=M/I。然后反向投影R,即使用R作为调色板,并以每个像素作为其对应的目标概率创建一个新图像。即B(x,y) = R[h(x,y),s(x,y)] 其中h是色调,s是像素在(x,y)的饱和度。之后,应用条件B(x,y) = min[B(x,y), 1]。
h,s,v = cv.split(hsvt)
B = R[h.ravel(),s.ravel()]
B = np.minimum(B,1)
B = B.reshape(hsvt.shape[:2])
3.现在对圆盘应用卷积,B = D * B,其中D是圆盘内核。
disc = cv.getStructuringElement(cv.MORPH_ELLIPSE,(5,5))
cv.filter2D(B,-1,disc,B)
B = np.uint8(B)
cv.normalize(B,B,0,255,cv.NORM_MINMAX)
4.现在最大强度的位置给了我们物体的位置。如果我们期望图像中有一个区域,则对合适的值进行阈值处理将获得不错的结果。
ret,thresh = cv.threshold(B,50,255,0)
就是这样!!
OpenCV提供了一个内建的函数cv.calcBackProject()。它的参数几乎与cv.calchist()函数相同。它的一个参数是直方图,也就是物体的直方图,我们必须找到它。另外,在传递给backproject函数之前,应该对对象直方图进行归一化。它返回概率图像。然后我们用圆盘内核对图像进行卷积并应用阈值。下面是我的代码和结果:
import numpy as np
import cv2 as cv
roi = cv.imread('rose_red.png')
hsv = cv.cvtColor(roi,cv.COLOR_BGR2HSV)
target = cv.imread('rose.png')
hsvt = cv.cvtColor(target,cv.COLOR_BGR2HSV)
# 计算对象的直方图
roihist = cv.calcHist([hsv],[0, 1], None, [180, 256], [0, 180, 0, 256] )
# 直方图归一化并利用反传算法
cv.normalize(roihist,roihist,0,255,cv.NORM_MINMAX)
dst = cv.calcBackProject([hsvt],[0,1],roihist,[0,180,0,256],1)
# 用圆盘进行卷积
disc = cv.getStructuringElement(cv.MORPH_ELLIPSE,(5,5))
cv.filter2D(dst,-1,disc,dst)
# 应用阈值作与操作
ret,thresh = cv.threshold(dst,50,255,0)
thresh = cv.merge((thresh,thresh,thresh))
res = cv.bitwise_and(target,thresh)
res = np.vstack((target,thresh,res))
cv.imwrite('res.jpg',res)
以下是我处理过的一个示例。我将蓝色矩形内的区域用作示例对象,我想提取整个地面。
在本节中,我们将学习
傅立叶变换用于分析各种滤波器的频率特性。对于图像,使用2D离散傅里叶变换(DFT)查找频域。一种称为快速傅立叶变换(FFT)的快速算法用于DFT的计算。关于这些的详细信息可以在任何图像处理或信号处理教科书中找到。请参阅其他资源部分。
对于正弦信号$x(t) = A sin(2 pi ft)$,我们可以说$f$是信号的频率,如果采用其频域,则可以看到$f$的尖峰。如果对信号进行采样以形成离散信号,我们将获得相同的频域,但是在$[-π,π]$或$[0,2π]$范围内(对于N点DFT为$[0,N]$)是周期性的。您可以将图像视为在两个方向上采样的信号。因此,在X和Y方向都进行傅立叶变换,可以得到图像的频率表示。
更直观地说,对于正弦信号,如果幅度在短时间内变化如此之快,则可以说它是高频信号。如果变化缓慢,则为低频信号。您可以将相同的想法扩展到图像。图像中的振幅在哪里急剧变化?在边缘点或噪声。因此,可以说边缘和噪声是图像中的高频内容。如果幅度没有太大变化,则它是低频分量。(一些链接已添加到“其他资源”,其中通过示例直观地说明了频率变换)。
现在,我们将看到如何找到傅立叶变换。
首先,我们将看到如何使用Numpy查找傅立叶变换。Numpy具有FFT软件包来执行此操作。np.fft.fft2()为我们提供了频率转换,它将是一个复杂的数组。它的第一个参数是输入图像,即灰度图像。第二个参数是可选的,它决定输出数组的大小。如果它大于输入图像的大小,则在计算FFT之前用零填充输入图像。如果小于输入图像,将裁切输入图像。如果未传递任何参数,则输出数组的大小将与输入的大小相同。
现在,一旦获得结果,零频率分量(DC分量)将位于左上角。如果要使其居中,则需要在两个方向上将结果都移动$frac{N}{2}$。只需通过函数np.fft.fftshift()即可完成。(它更容易分析)。找到频率变换后,就可以找到幅度谱。
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('messi5.jpg',0)
f = np.fft.fft2(img)
fshift = np.fft.fftshift(f)
magnitude_spectrum = 20*np.log(np.abs(fshift))
plt.subplot(121),plt.imshow(img, cmap = 'gray')
plt.title('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(magnitude_spectrum, cmap = 'gray')
plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
plt.show()
Result look like below:
结果看起来像下面这样:
看,您可以在中心看到更多白色区域,这表明低频内容更多。
因此,您发现了频率变换现在,您可以在频域中进行一些操作,例如高通滤波和重建图像,即找到逆DFT。为此,您只需用尺寸为60x60的矩形窗口遮罩即可消除低频。然后,使用np.fft.ifftshift()应用反向移位,以使DC分量再次出现在左上角。然后使用np.ifft2()函数找到逆FFT。同样,结果将是一个复数。您可以采用其绝对值。
rows, cols = img.shape
crow,ccol = rows//2 , cols//2
fshift[crow-30:crow+31, ccol-30:ccol+31] = 0
f_ishift = np.fft.ifftshift(fshift)
img_back = np.fft.ifft2(f_ishift)
img_back = np.real(img_back)
plt.subplot(131),plt.imshow(img, cmap = 'gray')
plt.title('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(132),plt.imshow(img_back, cmap = 'gray')
plt.title('Image after HPF'), plt.xticks([]), plt.yticks([])
plt.subplot(133),plt.imshow(img_back)
plt.title('Result in JET'), plt.xticks([]), plt.yticks([])
plt.show()
结果看起来像下面这样:
结果表明高通滤波是边缘检测操作。这就是我们在“图像渐变”一章中看到的。这也表明大多数图像数据都存在于频谱的低频区域。无论如何,我们已经看到了如何在Numpy中找到DFT,IDFT等。现在,让我们看看如何在OpenCV中进行操作。如果您仔细观察结果,尤其是最后一张JET颜色的图像,您会看到一些伪像(我用红色箭头标记的一个实例)。它在那里显示出一些波纹状结构,称为振铃效应。这是由我们用于遮罩的矩形窗口引起的。此掩码转换为正弦形状,从而导致此问题。因此,矩形窗口不用于过滤。更好的选择是高斯窗口。
OpenCV为此提供了cv.dft()和cv.idft()函数。它返回与前一个相同的结果,但是有两个通道。第一个通道是结果的实部,第二个通道是结果的虚部。输入图像首先应转换为np.float32。我们来看看怎么做。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('messi5.jpg',0)
dft = cv.dft(np.float32(img),flags = cv.DFT_COMPLEX_OUTPUT)
dft_shift = np.fft.fftshift(dft)
magnitude_spectrum = 20*np.log(cv.magnitude(dft_shift[:,:,0],dft_shift[:,:,1]))
plt.subplot(121),plt.imshow(img, cmap = 'gray')
plt.title('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(magnitude_spectrum, cmap = 'gray')
plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
plt.show()
注意
您还可以使用cv.cartToPolar(),它在单个镜头中同时返回幅值和相位
现在我们要做DFT的逆变换。在上一节中,我们创建了一个HPF,这次我们将看到如何删除图像中的高频内容,即我们将LPF应用到图像中。它实际上模糊了图像。为此,我们首先创建一个高值(1)在低频部分,即我们过滤低频内容,0在高频区。
rows, cols = img.shape
crow,ccol = rows/2 , cols/2
# 首先创建一个掩码,中心正方形为1,其余全为零
mask = np.zeros((rows,cols,2),np.uint8)
mask[crow-30:crow+30, ccol-30:ccol+30] = 1
# 应用掩码和逆DFT
fshift = dft_shift*mask
f_ishift = np.fft.ifftshift(fshift)
img_back = cv.idft(f_ishift)
img_back = cv.magnitude(img_back[:,:,0],img_back[:,:,1])
plt.subplot(121),plt.imshow(img, cmap = 'gray')
plt.title('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(img_back, cmap = 'gray')
plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
plt.show()
看看结果:
注意
通常,OpenCV函数cv.dft()和cv.idft()比Numpy函数更快。但是Numpy函数更容易使用。有关性能问题的更多细节,请参见下面的部分。
对于某些数组尺寸,DFT的计算性能较好。当数组大小为2的幂时,速度最快。对于大小为2、3和5的乘积的数组,也可以非常有效地进行处理。因此,如果您担心代码的性能,可以在找到DFT之前将数组的大小修改为任何最佳大小(通过填充零)。对于OpenCV,您必须手动填充零。但是对于Numpy,您指定FFT计算的新大小,它将自动为您填充零。
那么如何找到最优的大小呢?OpenCV为此提供了一个函数,cv.getOptimalDFTSize()。它同时适用于cv.dft()和np.fft.fft2()。让我们使用IPython魔术命令timeit来检查它们的性能。
In [16]: img = cv.imread('messi5.jpg',0)
In [17]: rows,cols = img.shape
In [18]: print("{} {}".format(rows,cols))
342 548
In [19]: nrows = cv.getOptimalDFTSize(rows)
In [20]: ncols = cv.getOptimalDFTSize(cols)
In [21]: print("{} {}".format(nrows,ncols))
360 576
参见,将大小(342,548)修改为(360,576)。现在让我们用零填充(对于OpenCV),并找到其DFT计算性能。您可以通过创建一个新的零数组并将数据复制到其中来完成此操作,或者使用cv.copyMakeBorder()。
nimg = np.zeros((nrows,ncols))
nimg[:rows,:cols] = img
或者:
right = ncols - cols
bottom = nrows - rows
bordertype = cv.BORDER_CONSTANT #只是为了避免PDF文件中的行中断
nimg = cv.copyMakeBorder(img,0,bottom,0,right,bordertype, value = 0)
现在,我们计算Numpy函数的DFT性能比较:
In [22]: %timeit fft1 = np.fft.fft2(img)
10 loops, best of 3: 40.9 ms per loop
In [23]: %timeit fft2 = np.fft.fft2(img,[nrows,ncols])
100 loops, best of 3: 10.4 ms per loop
它显示了4倍的加速。现在,我们将尝试使用OpenCV函数。
In [24]: %timeit dft1= cv.dft(np.float32(img),flags=cv.DFT_COMPLEX_OUTPUT)
100 loops, best of 3: 13.5 ms per loop
In [27]: %timeit dft2= cv.dft(np.float32(nimg),flags=cv.DFT_COMPLEX_OUTPUT)
100 loops, best of 3: 3.11 ms per loop
它还显示了4倍的加速。您还可以看到OpenCV函数比Numpy函数快3倍左右。也可以对逆FFT进行测试,这留给您练习。
在一个论坛上也有人提出了类似的问题。问题是,为什么拉普拉斯变换是高通滤波器?为什么Sobel是HPF?等。第一个答案是关于傅里叶变换的。对于更大的FFT只需要拉普拉斯变换。分析下面的代码:
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
# 没有缩放参数的简单均值滤波器
mean_filter = np.ones((3,3))
# 创建高斯滤波器
x = cv.getGaussianKernel(5,10)
gaussian = x*x.T
# 不同的边缘检测滤波器
# x方向上的scharr
scharr = np.array([[-3, 0, 3],
[-10,0,10],
[-3, 0, 3]])
# x方向上的sobel
sobel_x= np.array([[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]])
# y方向上的sobel
sobel_y= np.array([[-1,-2,-1],
[0, 0, 0],
[1, 2, 1]])
# 拉普拉斯变换
laplacian=np.array([[0, 1, 0],
[1,-4, 1],
[0, 1, 0]])
filters = [mean_filter, gaussian, laplacian, sobel_x, sobel_y, scharr]
filter_name = ['mean_filter', 'gaussian','laplacian', 'sobel_x',
'sobel_y', 'scharr_x']
fft_filters = [np.fft.fft2(x) for x in filters]
fft_shift = [np.fft.fftshift(y) for y in fft_filters]
mag_spectrum = [np.log(np.abs(z)+1) for z in fft_shift]
for i in xrange(6):
plt.subplot(2,3,i+1),plt.imshow(mag_spectrum[i],cmap = 'gray')
plt.title(filter_name[i]), plt.xticks([]), plt.yticks([])
plt.show()
看看结果:
从图像中,您可以看到每种内核阻止的频率区域以及它允许经过的区域。从这些信息中,我们可以说出为什么每个内核都是HPF或LPF
1.傅里叶变换的直观解释:
http://cns-alumni.bu.edu/~slehar/fourier/fourier.html by Steven Lehar
在本章中,您将学习
模板匹配是一种用于在较大图像中搜索和查找模板图像位置的方法。为此,OpenCV带有一个函数cv.matchTemplate()。它只是将模板图像滑动到输入图像上(就像在2D卷积中一样),然后在模板图像下比较模板和输入图像的拼图。OpenCV中实现了几种比较方法。(您可以检查文档以了解更多详细信息)。它返回一个灰度图像,其中每个像素表示该像素的邻域与模板匹配的程度。
如果输入图像的大小为(WxH),而模板图像的大小为(wxh),则输出图像的大小将为(W-w + 1,H-h + 1)。得到结果后,可以使用cv.minMaxLoc()函数查找最大/最小值在哪。将其作为矩形的左上角,并以(w,h)作为矩形的宽度和高度。该矩形是您模板的区域。
注意
如果使用cv.TM_SQDIFF作为比较方法,则最小值提供最佳匹配。
作为示例,我们将在梅西的照片中搜索他的脸。所以我创建了一个模板,如下所示:
我们将尝试所有比较方法,以便我们可以看到它们的结果如何:
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('messi5.jpg',0)
img2 = img.copy()
template = cv.imread('template.jpg',0)
w, h = template.shape[::-1]
# 列表中所有的6种比较方法
methods = ['cv.TM_CCOEFF', 'cv.TM_CCOEFF_NORMED', 'cv.TM_CCORR',
'cv.TM_CCORR_NORMED', 'cv.TM_SQDIFF', 'cv.TM_SQDIFF_NORMED']
for meth in methods:
img = img2.copy()
method = eval(meth)
# 应用模板匹配
res = cv.matchTemplate(img,template,method)
min_val, max_val, min_loc, max_loc = cv.minMaxLoc(res)
# 如果方法是TM_SQDIFF或TM_SQDIFF_NORMED,则取最小值
if method in [cv.TM_SQDIFF, cv.TM_SQDIFF_NORMED]:
top_left = min_loc
else:
top_left = max_loc
bottom_right = (top_left[0] + w, top_left[1] + h)
cv.rectangle(img,top_left, bottom_right, 255, 2)
plt.subplot(121),plt.imshow(res,cmap = 'gray')
plt.title('Matching Result'), plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(img,cmap = 'gray')
plt.title('Detected Point'), plt.xticks([]), plt.yticks([])
plt.suptitle(meth)
plt.show()
查看以下结果:
您会看到,使用cv.TM_CCORR的结果并不理想。
在上一节中,我们在图像中搜索了梅西的脸,该脸在图像中仅出现一次。假设您正在搜索具有多次出现的对象,则cv.minMaxLoc()不会为您提供所有位置。在这种情况下,我们将使用阈值化。因此,在此示例中,我们将使用著名游戏Mario的屏幕截图,并在其中找到硬币。
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img_rgb = cv.imread('mario.png')
img_gray = cv.cvtColor(img_rgb, cv.COLOR_BGR2GRAY)
template = cv.imread('mario_coin.png',0)
w, h = template.shape[::-1]
res = cv.matchTemplate(img_gray,template,cv.TM_CCOEFF_NORMED)
threshold = 0.8
loc = np.where( res >= threshold)
for pt in zip(*loc[::-1]):
cv.rectangle(img_rgb, pt, (pt[0] + w, pt[1] + h), (0,0,255), 2)
cv.imwrite('res.png',img_rgb)
结果:
在这一章当中,
如果可以用数学形式表示形状,则霍夫变换是一种检测任何形状的流行技术。即使形状有些破损或变形,也可以检测出形状。我们将看到它如何作用于一条线。
一条线可以表示为$y = mx + c$或以参数形式表示为$rho=xcosθ+ysinθ$,其中$rho$是从原点到该线的垂直距离,而$theta$是由该垂直线和水平轴形成的角度以逆时针方向测量(该方向随您如何表示坐标系而变化。此表示形式在OpenCV中使用)。查看下面的图片:
因此,如果线在原点下方通过,则它将具有正的$rho$且角度小于180。如果线在原点上方,则将角度取为小于180,而不是大于180的角度。$rho$取负值。任何垂直线将具有0度,水平线将具有90度。
现在,让我们看一下霍夫变换如何处理线条。任何一条线都可以用$(ρ,θ)$这两个术语表示。因此,首先创建2D数组或累加器(以保存两个参数的值),并将其初始设置为$0$。让行表示$ρ$,列表示$θ$。阵列的大小取决于所需的精度。假设您希望角度的精度为1度,则需要180列。对于$ρ$,最大距离可能是图像的对角线长度。因此,以一个像素精度为准,行数可以是图像的对角线长度。
考虑一个100x100的图像,中间有一条水平线。取直线的第一点。您知道它的(x,y)值。现在在线性方程式中,将值$θ$= 0,1,2,..... 180放进去,然后检查得到$ρ$。对于每对$(ρ,θ)$,在累加器中对应的$(ρ,θ)$单元格将值增加1。所以现在在累加器中,单元格(50,90)= 1以及其他一些单元格。
现在,对行的第二个点。执行与上述相同的操作。递增$(rho,theta)$对应的单元格中的值。这次,单元格(50,90)=2。实际上,您正在对$(ρ,θ)$值进行投票。您对线路上的每个点都继续执行此过程。在每个点上,单元格(50,90)都会增加或投票,而其他单元格可能会或可能不会投票。这样一来,最后,单元格(50,90)的投票数将最高。因此,如果您在累加器中搜索最大票数,则将获得(50,90)值,该值表示该图像中的一条线与原点的距离为50,角度为90度。在下面的动画中很好地显示了该图片(图片提供:Amos Storkey)
这就是霍夫变换对线条的工作方式。它很简单,也许您可以自己使用Numpy来实现它。下图显示了累加器。某些位置的亮点表示它们是图像中可能的线条的参数。(图片由维基百科提供)
上面说明的所有内容都封装在OpenCV函数cv.HoughLines()中。它只是返回一个:math:(rho,theta)值的数组。$ρ$以像素为单位,$θ$以弧度为单位。第一个参数,输入图像应该是二进制图像,因此在应用霍夫变换之前,请应用阈值或使用Canny边缘检测。第二和第三参数分别是$ρ$和$θ$精度。第四个参数是阈值,这意味着应该将其视为行的最低投票。请记住,票数取决于线上的点数。因此,它表示应检测到的最小线长。
import cv2 as cv
import numpy as np
img = cv.imread(cv.samples.findFile('sudoku.png'))
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
edges = cv.Canny(gray,50,150,apertureSize = 3)
lines = cv.HoughLines(edges,1,np.pi/180,200)
for line in lines:
rho,theta = line[0]
a = np.cos(theta)
b = np.sin(theta)
x0 = a*rho
y0 = b*rho
x1 = int(x0 + 1000*(-b))
y1 = int(y0 + 1000*(a))
x2 = int(x0 - 1000*(-b))
y2 = int(y0 - 1000*(a))
cv.line(img,(x1,y1),(x2,y2),(0,0,255),2)
cv.imwrite('houghlines3.jpg',img)
检查下面的结果
在霍夫变换中,您可以看到,即使对于带有两个参数的行,也需要大量计算。概率霍夫变换是我们看到的霍夫变换的优化。它没有考虑所有要点。取而代之的是,它仅采用随机的点子集,足以进行线检测。只是我们必须降低阈值。参见下图,比较了霍夫空间中的霍夫变换和概率霍夫变换。(图片提供:Franck Bettinger的主页)
OpenCV的实现基于Matas,J.和Galambos,C.和Kittler, J.V.使用渐进概率霍夫变换对行进行的稳健检测[145]。使用的函数是**cv.HoughLinesP**()。它有两个新的论点。
最好的是,它直接返回行的两个端点。在以前的情况下,您仅获得线的参数,并且必须找到所有点。在这里,一切都是直接而简单的。
import cv2 as cv
import numpy as np
img = cv.imread(cv.samples.findFile('sudoku.png'))
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
edges = cv.Canny(gray,50,150,apertureSize = 3)
lines = cv.HoughLinesP(edges,1,np.pi/180,100,minLineLength=100,maxLineGap=10)
for line in lines:
x1,y1,x2,y2 = line[0]
cv.line(img,(x1,y1),(x2,y2),(0,255,0),2)
cv.imwrite('houghlines5.jpg',img)
看到如下结果:
在本章中,
圆在数学上表示为(x-x_center)^2+(y-y_center)^2 = r^2,其中(x_center,y_center)是圆的中心,r是圆的半径。从等式中,我们可以看到我们有3个参数,因此我们需要3D累加器进行霍夫变换,这将非常低效。因此,OpenCV使用更加技巧性的方法,即使用边缘的梯度信息的Hough梯度方法。
我们在这里使用的函数是cv.HoughCircles()。它有很多参数,这些参数在文档中有很好的解释。因此,我们直接转到代码。
import numpy as np
import cv2 as cv
img = cv.imread('opencv-logo-white.png',0)
img = cv.medianBlur(img,5)
cimg = cv.cvtColor(img,cv.COLOR_GRAY2BGR)
circles = cv.HoughCircles(img,cv.HOUGH_GRADIENT,1,20,
param1=50,param2=30,minRadius=0,maxRadius=0)
circles = np.uint16(np.around(circles))
for i in circles[0,:]:
# 绘制外圆
cv.circle(cimg,(i[0],i[1]),i[2],(0,255,0),2)
# 绘制圆心
cv.circle(cimg,(i[0],i[1]),2,(0,0,255),3)
cv.imshow('detected circles',cimg)
cv.waitKey(0)
cv.destroyAllWindows()
结果如下:
在本章中,
任何灰度图像都可以看作是一个地形表面,其中高强度表示山峰,低强度表示山谷。你开始用不同颜色的水(标签)填充每个孤立的山谷(局部最小值)。随着水位的上升,根据附近的山峰(坡度),来自不同山谷的水明显会开始合并,颜色也不同。为了避免这种情况,你要在水融合的地方建造屏障。你继续填满水,建造障碍,直到所有的山峰都在水下。然后你创建的屏障将返回你的分割结果。这就是Watershed背后的“思想”。你可以访问Watershed的CMM网页,了解它与一些动画的帮助。
但是这种方法会由于图像中的噪声或其他不规则性而产生过度分割的结果。因此OpenCV实现了一个基于标记的分水岭算法,你可以指定哪些是要合并的山谷点,哪些不是。这是一个交互式的图像分割。我们所做的是给我们知道的对象赋予不同的标签。用一种颜色(或强度)标记我们确定为前景或对象的区域,用另一种颜色标记我们确定为背景或非对象的区域,最后用0标记我们不确定的区域。这是我们的标记。然后应用分水岭算法。然后我们的标记将使用我们给出的标签进行更新,对象的边界值将为-1。
下面我们将看到一个有关如何使用距离变换和分水岭来分割相互接触的对象的示例。
考虑下面的硬币图像,硬币彼此接触。即使你设置阈值,它也会彼此接触。
我们先从寻找硬币的近似估计开始。因此,我们可以使用Otsu的二值化。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('coins.png')
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
ret, thresh = cv.threshold(gray,0,255,cv.THRESH_BINARY_INV+cv.THRESH_OTSU)
现在我们需要去除图像中的任何白点噪声。为此,我们可以使用形态学扩张。要去除对象中的任何小孔,我们可以使用形态学侵蚀。因此,现在我们可以确定,靠近对象中心的区域是前景,而离对象中心很远的区域是背景。我们不确定的唯一区域是硬币的边界区域。
因此,我们需要提取我们可确定为硬币的区域。侵蚀会去除边界像素。因此,无论剩余多少,我们都可以肯定它是硬币。如果物体彼此不接触,那将起作用。但是,由于它们彼此接触,因此另一个好选择是找到距离变换并应用适当的阈值。接下来,我们需要找到我们确定它们不是硬币的区域。为此,我们扩张了结果。膨胀将对象边界增加到背景。这样,由于边界区域已删除,因此我们可以确保结果中背景中的任何区域实际上都是背景。参见下图。
剩下的区域是我们不知道的区域,无论是硬币还是背景。分水岭算法应该找到它。这些区域通常位于前景和背景相遇(甚至两个不同的硬币相遇)的硬币边界附近。我们称之为边界。可以通过从sure_bg区域中减去sure_fg区域来获得。
# 噪声去除
kernel = np.ones((3,3),np.uint8)
opening = cv.morphologyEx(thresh,cv.MORPH_OPEN,kernel, iterations = 2)
# 确定背景区域
sure_bg = cv.dilate(opening,kernel,iterations=3)
# 寻找前景区域
dist_transform = cv.distanceTransform(opening,cv.DIST_L2,5)
ret, sure_fg = cv.threshold(dist_transform,0.7*dist_transform.max(),255,0)
# 找到未知区域
sure_fg = np.uint8(sure_fg)
unknown = cv.subtract(sure_bg,sure_fg)
查看结果。在阈值图像中,我们得到了一些硬币区域,我们确定它们是硬币,并且现在已分离它们。(在某些情况下,你可能只对前景分割感兴趣,而不对分离相互接触的对象感兴趣。在那种情况下,你无需使用距离变换,只需侵蚀就足够了。侵蚀只是提取确定前景区域的另一种方法。)
现在我们可以确定哪些是硬币的区域,哪些是背景。因此,我们创建了标记(它的大小与原始图像的大小相同,但具有int32数据类型),并标记其中的区域。我们肯定知道的区域(无论是前景还是背景)都标有任何正整数,但是带有不同的整数,而我们不确定的区域则保留为零。为此,我们使用cv.connectedComponents()。它用0标记图像的背景,然后其他对象用从1开始的整数标记。
但是我们知道,如果背景标记为0,则分水岭会将其视为未知区域。所以我们想用不同的整数来标记它。相反,我们将未知定义的未知区域标记为0。
# 类别标记
ret, markers = cv.connectedComponents(sure_fg)
# 为所有的标记加1,保证背景是0而不是1
markers = markers+1
# 现在让所有的未知区域为0
markers[unknown==255] = 0
参见JET colormap中显示的结果。深蓝色区域显示未知区域。当然,硬币的颜色不同。剩下,肯定为背景的区域显示在较浅的蓝色,跟未知区域相比。
现在我们的标记已准备就绪。现在是最后一步的时候了,使用分水岭算法。然后标记图像将被修改。边界区域将标记为-1。
markers = cv.watershed(img,markers)
img[markers == -1] = [255,0,0]
请参阅下面的结果。对某些硬币,它们接触的区域被正确地分割,而对于某些硬币,却不是。
在本章中,
GrabCut算法由英国微软研究院的Carsten Rother,Vladimir Kolmogorov和Andrew Blake设计。在他们的论文“GrabCut”中:使用迭代图割的交互式前景提取。需要用最少的用户交互进行前景提取的算法,结果是GrabCut。
从用户角度来看,它是如何工作的?最初,用户在前景区域周围绘制一个矩形(前景区域应完全位于矩形内部)。然后,算法会对其进行迭代分割,以获得最佳结果。做完了但在某些情况下,分割可能不会很好,例如,可能已将某些前景区域标记为背景,反之亦然。在这种情况下,需要用户进行精修。只需在图像错误分割区域上画些笔画。笔画基本上说 “嘿,该区域应该是前景,你将其标记为背景,在下一次迭代中对其进行校正”或与背景相反。然后在下一次迭代中,你将获得更好的结果。
参见下图。第一名球员和橄榄球被封闭在一个蓝色矩形中。然后用白色笔划(表示前景)和黑色笔划(表示背景)进行最后的修饰。而且我们得到了不错的结果。
那么背景发生了什么呢?
如下图所示(图片提供:
http://www.cs.ru.ac.za/research/g02m1682/)
现在我们使用OpenCV进行抓取算法。OpenCV为此具有功能cv.grabCut(),我们将首先看到其参数:
首先让我们看看矩形模式。我们加载图像,创建类似的mask图像。我们创建fgdModel和bgdModel。我们给出矩形参数。一切都是直截了当的。让算法运行5次迭代。模式应为cv.GCINITWITH_RECT, 因为我们使用的是矩形。然后运行grabcut。修改mask图像。在新的mask图像中,像素将被标记有四个标记,分别表示上面指定的背景/前景。因此,我们修改mask,使所有0像素和2像素都置为0(即背景),而所有1像素和3像素均置为1(即前景像素)。现在,我们的最终mask已经准备就绪。只需将其与输入图像相乘即可得到分割的图像。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('messi5.jpg')
mask = np.zeros(img.shape[:2],np.uint8)
bgdModel = np.zeros((1,65),np.float64)
fgdModel = np.zeros((1,65),np.float64)
rect = (50,50,450,290)
cv.grabCut(img,mask,rect,bgdModel,fgdModel,5,cv.GC_INIT_WITH_RECT)
mask2 = np.where((mask==2)|(mask==0),0,1).astype('uint8')
img = img*mask2[:,:,np.newaxis]
plt.imshow(img),plt.colorbar(),plt.show()
查看以下结果:
糟糕,梅西的头发不见了。谁会喜欢没有头发的梅西?我们需要把它找回来。因此,我们将使用1像素(确保前景)进行精细修饰。同时,一些不需要的地面也出现在图片里。我们需要删除它们。在那里,我们给出了一些0像素的修饰(确保背景)。因此,如现在所说,我们在以前的情况下修改生成的mask。
我实际上所做的是,我在paint应用程序中打开了输入图像,并在图像中添加了另一层。使用画笔中的画笔工具,我在新图层上用白色标记了错过的前景(头发,鞋子,球等),而用白色标记了不需要的背景(例如logo,地面等)。然后用灰色填充剩余的背景。然后将该mask图像加载到OpenCV中,编辑我们在新添加的mask图像中具有相应值的原始mask图像。
检查以下代码:
#newmask是我手动标记过的mask图像
newmask = cv.imread('newmask.png',0)
# 标记为白色(确保前景)的地方,更改mask = 1
# 标记为黑色(确保背景)的地方,更改mask = 0
mask[newmask == 0] = 0
mask[newmask == 255] = 1
mask, bgdModel, fgdModel = cv.grabCut(img,mask,None,bgdModel,fgdModel,5,cv.GC_INIT_WITH_MASK)
mask = np.where((mask==2)|(mask==0),0,1).astype('uint8')
img = img*mask[:,:,np.newaxis]
plt.imshow(img),plt.colorbar(),plt.show()
查看以下结果:
就是这样了。在这里,你无需直接在rect模式下初始化,而可以直接进入mask模式。只需用2像素或3像素(可能的背景/前景)标记mask图像中的矩形区域。然后像在第二个示例中一样,将我们的sure_foreground标记为1像素。然后直接在mask模式下应用grabCut功能。
在本章中,我们将尝试理解什么是特征,为什么拐角重要等等
你们大多数人都会玩拼图游戏。你会得到很多小图像,需要正确组装它们以形成大的真实图像。问题是,你怎么做?将相同的理论投影到计算机程序上,以便计算机可以玩拼图游戏呢?如果计算机可以玩拼图游戏,为什么我们不能给计算机提供很多自然风光的真实图像,并告诉计算机将所有这些图像拼接成一个大图像呢?如果计算机可以将多个自然图像缝合在一起,那么如何给建筑物或任何结构提供大量图片并告诉计算机从中创建3D模型呢?
好了,问题和想象力还在继续。但这全都取决于最基本的问题:你如何玩拼图游戏?你如何将许多被扰的图像片段排列成一个大的单张图像?你如何将许多自然图像拼接到一张图像上?
答案是,我们正在寻找独特的,易于跟踪和比较的特定模板或特定特征。如果我们对这种特征进行定义,可能会发现很难用语言来表达它,但是我们知道它们是什么。如果有人要求你指出一项可以在多张图像中进行比较的良好特征,则可以指出其中一项。这就是为什么即使是小孩也可以玩这些游戏的原因。我们在图像中搜索这些特征,找到它们,在其他图像中寻找相同的特征并将它们对齐。仅此而已。(在拼图游戏中,我们更多地研究了不同图像的连续性)。所有这些属性都是我们固有的。
因此,我们的一个基本问题扩展到更多,但变得更加具体。这些特征是什么?(答案对于计算机也应该是可以理解的。)
很难说人类如何发现这些特征。这已经在我们的大脑中进行了编码。但是,如果我们深入研究某些图片并搜索不同的模板,我们会发现一些有趣的东西。例如,看以下的图片:
图像非常简单。在图像的顶部,给出了六个小图像块。你的问题是在原始图像中找到这些补丁的确切位置。你可以找到多少正确的结果?
A和B是平坦的表面,它们散布在很多区域上。很难找到这些补丁的确切位置。
C和D更简单。它们是建筑物的边缘。你可以找到一个大概的位置,但是准确的位置仍然很困难。这是因为沿着边缘的每个地方的图案都是相同的。但是,在边缘,情况有所不同。因此,与平坦区域相比,边缘是更好的特征,但不够好(在拼图游戏中比较边缘的连续性很好)。
最后,E和F是建筑物的某些角落。而且很容易找到它们。因为在拐角处,无论将此修补程序移动到何处,它的外观都将有所不同。因此,它们可以被视为很好的特征。因此,现在我们进入更简单(且被广泛使用的图像)以更好地理解。
就像上面一样,蓝色补丁是平坦区域,很难找到和跟踪。无论你将蓝色补丁移到何处,它看起来都一样。黑色补丁有一个边缘。如果你沿垂直方向(即沿渐变)移动它,则它会发生变化。沿着边缘(平行于边缘)移动,看起来相同。对于红色补丁,这是一个角落。无论你将补丁移动到何处,它看起来都不同,这意味着它是唯一的。因此,基本上,拐点被认为是图像中的良好特征。(不仅是角落,在某些情况下,斑点也被认为是不错的功能)。
因此,现在我们回答了我们的问题,“这些特征是什么?”。但是出现了下一个问题。我们如何找到它们?还是我们如何找到角落?我们以一种直观的方式回答了这一问题,即寻找图像中在其周围所有区域中移动(少量)变化最大的区域。在接下来的章节中,这将被投影到计算机语言中。因此,找到这些图像特征称为特征检测。
我们在图像中找到了特征。找到它之后,你应该能够在其他图像中找到相同的图像。怎么做?我们围绕该特征采取一个区域,我们用自己的语言解释它,例如“上部是蓝天,下部是建筑物的区域,在建筑物上有玻璃等”,而你在另一个建筑物中搜索相同的区域图片。基本上,你是在描述特征。同样,计算机还应该描述特征周围的区域,以便可以在其他图像中找到它。所谓的描述称为特征描述。获得特征及其描述后,你可以在所有图像中找到相同的功能并将它们对齐,缝合在一起或进行所需的操作。
因此,在此模块中,我们正在寻找OpenCV中的不同算法来查找功能,对其进行描述,进行匹配等。
在本章中,
在上一章中,我们看到了Harris Corner Detector。1994年下半年,J。Shi和C. Tomasi在他们的论文《有益于跟踪的特征》中做了一个小修改,与Harris Harris Detector相比,显示了更好的结果。哈里斯角落探测器的计分功能由下式给出:
取而代之的是,史托马西提出:
如果大于阈值,则将其视为拐角。如果像在Harris Corner Detector中那样在$lambda1-lambda2$空间中绘制它,则会得到如下图像:
从图中可以看到,只有当λ_1和λ_2大于最小值λ_min时,才将其视为拐角(绿色区域)。
OpenCV有一个函数cv.goodFeaturesToTrack()。它通过Shi-Tomasi方法(或哈里斯角检测,如果指定)找到图像中的N个最强角。像往常一样,图像应该是灰度图像。然后,指定要查找的角数。然后,您指定质量级别,该值是介于0-1之间的值,该值表示每个角落都被拒绝的最低拐角质量。然后,我们提供检测到的角之间的最小欧式距离。利用所有这些信息,该功能可以找到图像中的拐角。低于平均质量的所有拐角点均被拒绝。然后,它会根据质量以降序对剩余的角进行排序。然后函数首先获取最佳拐角,然后丢弃最小距离范围内的所有附近拐角,然后返回N个最佳拐角。在下面的示例中,我们将尝试找到25个最佳弯角:
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('blox.jpg')
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
corners = cv.goodFeaturesToTrack(gray,25,0.01,10)
corners = np.int0(corners)
for i in corners:
x,y = i.ravel()
cv.circle(img,(x,y),3,255,-1)
plt.imshow(img),plt.show()
查看以下结果:
此功能更适合跟踪。我们将看到使用它的时机
在这一章当中,
在前两章中,我们看到了一些像Harris这样的拐角检测器。它们是旋转不变的,这意味着即使图像旋转了,我们也可以找到相同的角。很明显,因为转角在旋转的图像中也仍然是转角。但是缩放呢?如果缩放图像,则拐角可能不是角。例如,检查下面的简单图像。在同一窗口中放大小窗口中小图像中的拐角时,该角是平坦的。因此,Harris拐角不是尺度不变的。
因此,在2004年,不列颠哥伦比亚大学的D.Lowe在他的论文《尺度不变关键点中的独特图像特征》中提出了一种新算法,即尺度不变特征变换(SIFT),该算法提取关键点并计算其描述算符。 (改论文易于理解,被认为是学习SIFT的最佳材料。因此,本文只是该论文的简短摘要)。 SIFT算法主要包括四个步骤。 我们将一一看到它们。
SIFT算法主要包括四个步骤。我们将一一看到它们。
从上图可以明显看出,我们不能使用相同的窗口来检测具有不同比例的关键点。即便小拐角可以。但是要检测更大的拐角,我们将需要更大的窗口。为此,使用了比例空间滤波。在其中,找到具有各种$σ$值的图像的高斯拉普拉斯算子。LoG用作斑点检测器,可检测由于$σ$的变化而导致的各种大小的斑点。简而言之,$σ$用作缩放参数。例如,在上图中,低$σ$的高斯核对于较小的拐角给出较高的值,而高$σ$的高斯核对于较大的拐角而言非常合适。因此,我们可以找到整个尺度和空间上的局部最大值,这给了我们$(x,y,σ)$值的列表,这意味着在$(x,y)$在$σ$尺度上有一个潜在的关键点。
但是这种LoG代价昂贵,因此SIFT算法使用的是高斯差值,它是LoG的近似值。高斯差是作为具有两个不同$σ$的图像的高斯模糊差而获得的,设为$σ$和$kσ$。此过程是针对高斯金字塔中图像的不同八度完成的。如下图所示:
一旦找到该DoG,便会在图像上搜索比例和空间上的局部极值。例如,将图像中的一个像素与其8个相邻像素以及下一个比例的9个像素和前一个比例的9个像素进行比较。如果是局部极值,则可能是关键点。从根本上说,关键点是最好的代表。如下图所示:
对于不同的参数,本文给出了一些经验数据,可以概括为:octaves=4,缩放尺度=5,初始$σ=1.6$,$k=sqrt{2}$等作为最佳值。
一旦找到潜在的关键点位置,就必须对其进行优化以获取更准确的结果。他们使用了标度空间的泰勒级数展开来获得更精确的极值位置,如果该极值处的强度小于阈值(根据论文为0.03),则将其拒绝。在OpenCV DoG中,此阈值称为ContrastThreshold,它对边缘的响应较高,因此也需要删除边缘。
为此,使用类似于哈里斯拐角检测器的概念。他们使用2x2的Hessian矩阵(H)计算主曲率。从哈里斯拐角检测器我们知道,对于边缘,一个特征值大于另一个特征值。因此,这里他们使用了一个简单的函数。
如果该比率大于一个阈值(在OpenCV中称为edgeThreshold),则该关键点将被丢弃。论文上写的值为10。
因此,它消除了任何低对比度的关键点和边缘关键点,剩下的就是很可能的目标点。
现在,将方向分配给每个关键点,以实现图像旋转的不变性。根据比例在关键点位置附近采取邻域,并在该区域中计算梯度大小和方向。创建了一个具有36个覆盖360度的bin的方向直方图(通过梯度幅度和$σ$等于关键点比例的1.5的高斯加权圆窗加权)。提取直方图中的最高峰,并且将其超过80%的任何峰也视为计算方向。它创建的位置和比例相同但方向不同的关键点。它有助于匹配的稳定性。
现在创建了关键点描述符。在关键点周围采用了16x16的邻域。它分为16个4x4大小的子块。对于每个子块,创建8 bin方向直方图。因此共有128个bin值可用。它被表示为形成关键点描述符的向量。除此之外,还采取了几种措施来实现针对照明变化,旋转等的鲁棒性。
通过识别两个图像的最近邻,可以匹配两个图像之间的关键点。但是在某些情况下,第二个最接近的匹配可能非常接近第一个。它可能是由于噪音或其他原因而发生的。在那种情况下,采用最接近距离与第二最接近距离之比。如果大于0.8,将被拒绝。根据论文,它可以消除大约90%的错误匹配,而仅丢弃5%的正确匹配。因此,这是SIFT算法的总结。有关更多详细信息和理解,强烈建议阅读原始论文。记住一件事,该算法已申请专利。所以这个算法包含在opencv contrib repo中
现在,让我们来看一下OpenCV中可用的SIFT功能。让我们从关键点检测开始并进行绘制。首先,我们必须构造一个SIFT对象。我们可以将不同的参数传递给它,这些参数是可选的,它们在docs中已得到很好的解释。
import numpy as np
import cv2 as cv
img = cv.imread('home.jpg')
gray= cv.cvtColor(img,cv.COLOR_BGR2GRAY)
sift = cv.xfeatures2d.SIFT_create()
kp = sift.detect(gray,None)
img=cv.drawKeypoints(gray,kp,img)
cv.imwrite('sift_keypoints.jpg',img)
sift.detect()函数在图像中找到关键点。如果只想搜索图像的一部分,则可以通过掩码。每个关键点是一个特殊的结构,具有许多属性,例如其(x,y)坐标,有意义的邻域的大小,指定其方向的角度,指定关键点强度的响应等。
OpenCV还提供cv.drawKeyPoints()函数,该函数在关键点的位置绘制小圆圈。如果将标志
cv.DRAWMATCHESFLAGSDRAWRICH_KEYPOINTS传递给它,它将绘制一个具有关键点大小的圆,甚至会显示其方向。请参见以下示例。
img=cv.drawKeypoints(gray,kp,img,flags=cv.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) cv.imwrite('sift_keypoints.jpg',img)
查看下面的结果:
现在要计算描述符,OpenCV提供了两种方法。
我们将看到第二种方法:
sift = cv.xfeatures2d.SIFT_create()
kp, des = sift.detectAndCompute(gray,None)
这里的kp将是一个关键点列表,而des是一个形状为$NumberofKeypoints×128$的数字数组。
这样我们得到了关键点,描述符等。现在我们想看看如何在不同图像中匹配关键点。我们将在接下来的章节中学习。
OpenCV是计算机视觉中经典的专用库,然而其中文版官方教程久久不来。近日,一款最新OpenCV4.1 版本的完整中文版官方教程出炉,读者朋友可以更好的学习了解OpenCV相关细节。教程来自objectdetection.cn。
最新Opencv-Python中文版官方文档:
http://woshicver.com
教程根据官方提供的文档,尽量完整的进行了还原。包括简单的OpenCV-Python安装,如何显示和保存图像和视频,图像的基本操作,OpenCV内部的不同图像处理函数,有关特征检测和描述符的信息。以及机器学习与目标检测部分等等。
部分内容如下
4_1_改变颜色空间
5_1_理解特征
最新Opencv-Python中文版官方文档:
http://woshicver.com
原创:人工智能遇见磐创
编辑:IT智能化专栏编辑