OpenCV实战(1)——OpenCV与图像处理
0. 前言
OpenCV
是一个的跨平台计算机视觉库,包含了 500
多个用于图像和视频分析的高效算法。本节将介绍 OpenCV
的基础知识,以及如何编译运行 OpenCV
程序,并将学习如何完成最基本的图像处理任务——读取、显示和保存图像。除此之外,鼠标事件和图形绘制也是 OpenCV
计算机视觉项目中常用的基本功能(例如图像标注场景,利用鼠标事件在图像中绘制目标对象的边界框),本节介绍了如何使用这两个重要的 OpenCV
功能。
1. OpenCV 基础
1.1 安装 OpenCV
OpenCV
是一个开源库,可以用于开发在 Windows
、Linux
、Android
和 macOS
等平台上运行的计算机视觉应用程序。自 1999
年推出以来,它已成为计算机视觉领域广泛采用的主要开发工具。在 OpenCV 网站,根据所用计算机的不同平台( Unix/Windows
或 Android
) 下载相对应的 OpenCV
包,关于不同平台 OpenCV
的安装方式可以参考官方指南或相关博客。
1.2 OpenCV 主要模块
从 OpenCV 2.2
版开始,OpenCV
库被分成了多个模块,这些模块是位于 lib
目录中的内置库文件,一些常用的模块如下:
-
core
模块包含OpenCV
库的核心函数,主要包括基本数据结构和算术函数 -
imgproc
模块包含主要的图像处理函数 -
highgui
模块包含图像和视频读写函数以及一些用户界面函数 -
features2d
模块包含特征点检测器和描述符以及特征点匹配框架 -
calib3d
模块包含相机校准、视图几何估计和立体函数 -
video
模块包含运动估计、特征跟踪和前景提取函数和类 -
objdetect
模块包含人脸检测和人物检测等目标检测函数
OpenCV
还包括许多其他实用模块,例如机器学习函数 (ml
)、计算几何算法 (flann
)等;除此之外,还包括其他实现更高级函数的专用库,例如,用于计算摄影的 photo
和用于图像拼接算法的 stitching
。
所有这些模块都有一个与之关联的头文件,因此,典型的 OpenCV C++
代码首先应当声明引入所需的模块,例如,建议的声明样式类似于以下代码:
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
有时,我们也可能会看到以以下命令开头的 OpenCV
代码,这是由于为了与旧定义兼容而使用了旧样式:
#include "cv.h"
1.3 使用 Qt 进行 OpenCV 开发
Qt
是作为开源项目开发的 C++
应用程序的跨平台 IDE
。它由两个独立的元素组成——一个名为 Qt Creator
的跨平台 IDE
,以及一组 Qt
类和开发工具。使用 Qt
开发 C++
应用程序有以下优点:
- 它是一个由
Qt
社区开发的开源计划,可以访问不同Qt
组件的源代码 - 它是一个跨平台的
IDE
,可以开发能够运行在不同操作系统上的应用程序,例如Windows
、Linux
、macOS
等 - 它包含一个完整的跨平台
GUI
库,遵循面向对象和事件驱动模型 -
Qt
还包括多个跨平台库,可用于开发多媒体、数据库、多线程、Web
应用程序以及其他高级应用程序
使用 Qt
可以很方便的编译 OpenCV
库,因为它可以读取 CMake
文件。一旦安装了 OpenCV
和 CMake
,只需从 Qt
的 File
菜单中选择 Open File or Project...
,然后打开 OpenCV
的源目录下的 CMakeLists.txt
文件,然后在弹出窗口中单击 configure project
:
创建一个 OpenCV
项目后,可以通过单击 Qt
菜单中的 Build Project
来构建该项目:
2. OpenCV 图像处理基础
一切准备就绪,接下来是运行第一个 OpenCV
应用程序的时候了。由于 OpenCV
的核心就是处理图像,因此我们首先学习如何执行图像应用程序所需的最基本操作,即从文件系统中加载输入图像、在窗口中显示图像、应用处理函数以及将输出图像存储在磁盘上。
2.1 加载、显示和保存图像
创建新文件 hello_opencv.cpp
,包含头文件,声明将使用的类和函数。由于,本节我们只需要显示一个图像,所以需要声明图像数据结构的 core
头文件和包含所有图形界面功能的 highgui
头文件:
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
2.1.1 加载图像
在 mian
函数中首先声明一个保存图像的变量,定义一个 cv::Mat
类对象:
cv::Mat image; // 创建一个空图像
根据此定义将创建一个大小为 0 x 0
的图像,可以通过访问 cv::Mat size
属性来确认图像尺寸:
std::cout << "This image is " << image.rows << " x " << image.cols << std::endl;
接下来,调用 imread
函数将从文件中读取图像,对其进行解码并分配内存:
image = cv::imread("1.png"); // 读取图像
当使用 imread
打开图像而不指定完整路径时,将使用默认目录。当直接运行应用程序时,可执行文件显然位于该目录下;但是,如果直接从 IDE
运行应用程序,默认目录通常是包含项目文件的目录,因此,需要确保输入图像文件位于正确的目录中。
读取图像后就可以使用此图像了。但是,为了保证图像被正确读取(如果找不到文件、文件是否已损坏或不是可识别的格式,则会出现错误),应该首先使用 empty()
函数检查图像是否正确读取。如果没有分配图像数据,empty()
方法返回 true
:
if (image.empty()) { // 异常处理
return 0;
}
2.1.2 显示图像
可以通过使用 highgui
模块中的函数来显示图像。首先需要声明用于显示图像的窗口,然后指定要在此窗口上显示的图像:
cv::namedWindow("Orginal Image");
cv::imshow("Orginal Image", image);
窗口由名称标识,我们也可以重复使用此窗口来显示另一个图像,或者可以创建多个具有不同名称的窗口。运行此应用程序时,可以看到一个图像窗口,如下所示:
加载图像后,通常需要对图像进行一些处理。OpenCV
提供了广泛的图像处理函数,例如,我们可以使用一个非常简单的 flip()
函数水平翻转图像。OpenCV
中的许多图像转换操作可以原地 (in-place
) 执行,这意味着转换可以直接应用于输入图像,而不必创建新图像。翻转操作就属于原地操作:
cv::flip(image,image,1); // 原地操作
但是,我们也可以创建另一个矩阵来保存输出结果:
cv::Mat result;
cv::flip(image, result, 1); // flip 函数中,正数表示水平翻转;0表示垂直翻转;负数表示同时进行水平和垂直翻转
在另一个窗口中显示图像翻转后的结果:
cv::namedWindow("Output Image");
cv::imshow("Output Image", result);
由于控制台窗口在 main
函数结束时就会终止,因此我们添加一个额外的 highgui
库函数以在结束程序之前等待用户按键操作:
cv::waitKey(0);
可以看到输出图像显示在一个不同的窗口中,如下图所示:
2.1.3 保存图像
最后,将处理后的图像保存在磁盘上,可以使用 highgui
库函数完成图像保存操作:
cv::imwrite("output.png", result); // 保存处理结果
文件扩展名决定了将使用哪个编解码器来保存图像,常见的图像格式包括 BMP
、JPG
、TIFF
和 PNG
等。
2.1.4 完整代码
完整代码如下所示:
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
int main(){
cv::Mat image; // 创建一个空图像
std::cout << "This image is " << image.rows << " x " << image.cols << std::endl;
image = cv::imread("1.png"); // 读取图像
if (image.empty()) { // 异常处理
std::cout << "Error reading image..." << std::endl;
return 0;
}
cv::namedWindow("Orginal Image");
cv::imshow("Orginal Image", image);
cv::waitKey(0);
cv::Mat result;
cv::flip(image, result, 1); // 正数表示水平翻转;0表示垂直翻转;负数表示同时进行水平和垂直翻转
cv::namedWindow("Output Image");
cv::imshow("Output Image", result);
cv::imwrite("output.png", result); // 保存处理结果
cv::waitKey(0);
return 0;
}
2.2 OpenCV 命名空间
OpenCV
的 C++ API
中的所有类和函数都定义在 cv
命名空间中。可以通过两种方式访问这些类和函数。第一种方法是在 main
函数的定义之前添加以下声明:
using namespace cv;
第二种方法是,按照命名空间规范(即 cv::
)作为所有 OpenCV
类和函数名称的前缀,前缀的使用使 OpenCV
类和函数更容易识别。
2.3 cv::imread() 函数详解
highgui
模块包含一组用于可视化图像并与之交互的函数,使用 imread
函数加载图像时,还可以选择将其作为灰度图像读取,这对于某些需要灰度图像的计算机视觉算法而言是十分有用的。在读取图像时即时转换输入的彩色图像可以节省时间并最大限度地减少内存使用:
// 将输入图片读取为灰度图像
image= cv::imread("1.png", cv::IMREAD_GRAYSCALE);
使用以上代码可以得到一个由无符号字节 (C++
中的 unsigned char
) 组成的图像,OpenCV
用 CV_8U
定义的常量表示这种数据类型。
有时即使输入图像为灰度图像,也需要将图像读取为三通道彩色图像。这可以通过设定第二个参数为正数调用 imread
函数来实现:
// 将输入图像读取为三通道彩色图像
image= cv::imread("1.png", cv::IMREAD_COLOR);
此时,可以得到一个每个像素由三个字节组成的图像,在 OpenCV
中指定为 CV_8UC3
,如果输入图像为灰度图像,则所有三个通道都将包含相同的值。最后,如果希望以保存时的格式读取图像,只需输入一个负值作为 imread
的第二个参数。可以使用 channels
方法检查图像中的通道数:
std::cout << "This image has " << image.channels() << " channel(s)";
当使用 imshow
显示由整数组成的图像 (16
位无符号整数指定为 CV_16U
,32
位有符号整数指定为 CV_32S
) 时,该图像的像素值将首先除以 256
,以尝试使其可显示为 256
种灰度值。同样,由浮点数组成的图像通过使用 0.0
(显示为黑色)和 1.0
(显示为白色)之间的可能值来显示。超出此定义范围的值以白色(对于高于 1.0
的值)或黑色(对于低于 0.0
的值)显示。
2.4 OpenCV 应用程序的编译执行
2.4.1 编译 OpenCV 应用程序
程序编写完成后,需要进行编译后才能执行,在大多数 IDE
中编写程序时,可以很方便的编译并执行,除此之外,我们还可以使用命令行编译并执行,我们主要介绍以下两种编译方法。
1. 方法一,通过g++命令进行编译得到可执行文件:
$ g++ hello_opencv.cpp -o hello_opencv `pkg-config --cflags --libs opencv`
在以上编译命令中,hello_opencv.cpp
是源文件,-o
选项用于指定编译后生成的输出文件 hello_opencv
,pkg-config
具有以下用途:
- 检查库的版本号,避免链接错误版本的库文件
- 获得编译预处理参数,例如头文件位置等
- 获得链接参数,例如库及其依赖库的位置、文件名和其他链接参数
- 自动加入所依赖的其他库的位置
在安装 OpenCV
的安装链接库文件目录 lib
中包含一个 pkgconfig
目录,其中包含一个 opencv.pc
文件,该文件即为 pkg-config
下的 OpenCV
配置文件,使用 pkg-config
时,选项 –cflags
用来指定程序在编译时所需要头文件所在的目录,选项 –libs
则指定程序在链接时所需要的动态链接库的目录。
2. 方法二,通过 cmake
进行编译,编辑 CMakeLists.txt
文件,添加以下代码:
#指定需要的cmake的最低版本
cmake_minimum_required(VERSION 2.8)
#创建工程
project(hello_opencv)
#指定C++语言为C++ 11
set(CMAKE_CXX_FLAGS "-std=c++11")
#查找OpenCV 安装路径
find_package(OpenCV REQUIRED)
#引入OpenCV头文件路径
include_directories(${OpenCV_INCLUDE_DIRS})
#指定编译 hello_opencv.cpp 程序编译后生成可执行文件 hello_opencv
add_executable(hello_opencv hello_opencv.cpp)
#指定可执行文件 hello_opencv 链接OpenCV lib
target_link_libraries(hello_opencv ${OpenCV_LIBS})
文件中各行代码作用使用注释进行说明,编写完成后,执行以下代码进行编译:
$ mkdir build
$ cd build
$ cmake ..
$ make
2.4.1 执行 OpenCV 应用程序
编译完成后,执行可执行程序:
$ ./hello_opencv
3. OpenCV 鼠标事件
highgui
模块包含一组丰富的函数,可用于与图像进行交互。使用这些函数,应用程序可以对鼠标或按键事件做出响应。
当鼠标位于所创建的图像窗口上时,通过定义回调函数,可以对鼠标进行编程以执行特定操作。回调函数是没有显式调用的函数,但它会被应用程序调用以响应特定事件(例如鼠标与图像窗口交互的事件) 。为了被应用程序识别,回调函数需要有一个特定的签名并且必须被注册。在鼠标事件处理程序的情况下,回调函数必须具有以下签名:
void onMouse(int event, int x, int y, int flags, void* param);
第一个参数 event
是一个整数,用于指定哪种类型的鼠标事件触发了对回调函数的调用;紧接着的两个参数 x
和 y
是事件发生时鼠标位置的像素坐标;最后一个参数用于以指向对象的指针的形式向函数发送一个额外的参数。可以通过以下方式在应用程序中注册此回调函数:
cv::setMouseCallback("Original Image", onMouse, reinterpret_cast<void*>(&image));
其中,onMouse
函数与名为 Original Image
的图像窗口相关联,并且需要图像的地址作为额外参数传递给该函数。如果我们定义如下代码所示的 onMouse
回调函数,那么每次点击鼠标时,都会在控制台上显示相应像素的值(假设图像是灰度图像) :
void onMouse(int event, int x, int y, int flags, void* param){
cv::Mat *im = reinterpret_cast<cv::Mat*>(param);
switch (event){
case cv::EVENT_LBUTTONDOWN:
// 打印坐标为 (x, y) 处的像素坐标
std::cout << "at (" << x << "," << y << ") value is: " << static_cast<int>(im->at<uchar>(cv::Point(x, y))) << std::endl;
break;
}
}
为了获得 (x,y)
处的像素值,我们使用了 cv::Mat
对象的 at()
方法,在之后的学习中会进行详细介绍,此处的重点在于介绍鼠标事件。鼠标事件回调函数可以接收的其他事件包括 cv::EVENT_MOUSE_MOVE
、cv::EVENT_LBUTTONUP
、cv::EVENT_RBUTTONDOWN
和 cv::EVENT_RBUTTONUP
等。
完整代码 ( mouse_event.cpp`) 如下所示:
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
void onMouse( int event, int x, int y, int flags, void* param) {
cv::Mat *im= reinterpret_cast<cv::Mat*>(param);
switch (event) { // dispatch the event
case cv::EVENT_LBUTTONDOWN: // mouse button down event
// display pixel value at (x,y)
std::cout << "at (" << x << "," << y << ") value is: "
<< static_cast<int>(im->at<uchar>(cv::Point(x,y))) << std::endl;
break;
}
}
int main() {
cv::Mat image; // 异常处理
std::cout << "This image is " << image.rows << " x "
<< image.cols << std::endl;
// 将输入图像读取为灰度图像
image= cv::imread("1.png", cv::IMREAD_GRAYSCALE);
if (image.empty()) { // error handling
std::cout << "Error reading image..." << std::endl;
return 0;
}
std::cout << "This image is " << image.rows << " x "
<< image.cols << std::endl;
std::cout << "This image has "
<< image.channels() << " channel(s)" << std::endl;
// 创建窗口,显示图像
cv::namedWindow("Original Image");
cv::imshow("Original Image", image);
// 为图像设置鼠标回调函数
cv::setMouseCallback("Original Image", onMouse, reinterpret_cast<void*>(&image));
cv::waitKey(0); // 等待键盘事件
return 0;
}
编译并执行程序,结果如下所示:
$ g++ mouse_event.cpp -o mouse_event `pkg-config --cflags --libs opencv`
$ ./mouse_event
4. OpenCV 绘制图形
OpenCV highgui
模块还提供了一些在图像上绘制图形和书写文本的函数。基本形状绘制函数包括circle()
、ellipse()
、line()
和 rectangle()
等,以 circle()
函数为例,其他绘图函数的基本用法类似:
cv::circle(image, // 绘制的目标图像
cv::Point(155, 110), // 圆心坐标
65, // 半径
0, // 颜色
3, // 线条粗细
);
cv::Point
结构常用于 OpenCV
方法和函数中来指定像素坐标,我们假设绘制是在灰度图像上完成的;因此我们使用单个整数 0
指定绘制颜色。在之后的学习中,我们会学习如何在使用 cv::Scalar
结构的彩色图像的情况下指定颜色值。我们也可以在图像上写入文本:
cv::putText(image, // 绘制的目标图像
"This is a dog.", // 文本
cv::Point(40, 200), // 文本位置
cv::FONT_HERSHEY_PLAIN, // 字体类型
2.0, // 缩放比例
255, // 文字颜色
2, // 文字粗细
)
完整代码 (draw_on_image.cpp
) 如下所示:
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
int main() {
cv::Mat image; // create an empty image
std::cout << "This image is " << image.rows << " x "
<< image.cols << std::endl;
// 读取图像
image= cv::imread("1.png", cv::IMREAD_GRAYSCALE);
// 创建图像窗口
cv::namedWindow("Drawing on an Image");
// 绘制圆形
cv::circle(image, // 目标图像
cv::Point(430,160), // 圆心坐标
150, // 半径
0, // 颜色
3); // 线条粗细
// 绘制文本
cv::putText(image, // 目标图像
"This is a person.", // 文本
cv::Point(280,350), // 文本位置
cv::FONT_HERSHEY_PLAIN, // 字体类型
2.0, // 缩放
255, // 字体颜色
2); // 字体粗细
cv::imshow("Drawing on an Image", image); // 显示图像
cv::waitKey(0); // 等待键盘响应
return 0;
}
编译并执行代码,在测试图像上调用这两个函数可以得到以下结果:
$ g++ draw_on_image.cpp -o draw_on_image `pkg-config --cflags --libs opencv`
$ ./draw_on_image
5. 使用 Qt 运行 OpenCV 应用程序
如果想要使用 Qt
来运行 OpenCV
应用程序,需要创建项目文件。例如,对于上一小节的示例,项目文件 (draw_on_image.pro
) 如下所示:
# 库模板
TEMPLATE = app
# 目标文件
TARGET = draw_on_image
# 项目目录
INCLUDEPATH += .
# 向qmake声明应用程序依赖于 widgets 模块
greaterThan(QT_MAJOR_VERSION,4): QT += widgets
# OpenCV 安装路径
unix:!mac {
INCLUDEPATH += /home/brainiac/Program/opencv4/include/opencv4
LIBS += -L/home/brainiac/Program/opencv4/lib -lopencv_core -lopencv_imgproc -lopencv_highgui -lopencv_imgcodecs
}
WIN32 {
INCLUDEPATH += C:/OpenCV4.6.0/build/include/opencv4
LIBS += -lc:/OpenCV4.6.0/build/lib/ -lopencv_core -lopencv_imgproc -lopencv_highgui -lopencv_imgcodecs
}
# 源文件
SOURCES += draw_on_image.cpp
该文件声明了文件和库文件的位置,还列出了代码使用的库模块。确保使用与 Qt
正在使用的编译器兼容的库二进制文件。
使用 qmake
编译并执行应用程序,结果如下所示:
$ qmake -makefile
$ make
$ ./draw_on_image
小结
OpenCV
是一个跨平台计算机视觉和机器学习库,实现了图像处理和计算机视觉方面的很多通用算法。本文,首先介绍了 OpenCV
的基础知识,并介绍了如何在不同平台安装 OpenCV
库,同时演示了如何使用不同方式编写、编译和执行 OpenCV
应用程序。然后,我们还总结了 OpenCV
处理图像的基础函数和功能,包括读取、显示和保存图像,以及鼠标事件和图像绘制。