OpenCV实战(1)——OpenCV与图像处理

时间:2022-10-24 13:54:07

0. 前言

OpenCV 是一个的跨平台计算机视觉库,包含了 500 多个用于图像和视频分析的高效算法。本节将介绍 OpenCV 的基础知识,以及如何编译运行 OpenCV 程序,并将学习如何完成最基本的图像处理任务——读取、显示和保存图像。除此之外,鼠标事件和图形绘制也是 OpenCV 计算机视觉项目中常用的基本功能(例如图像标注场景,利用鼠标事件在图像中绘制目标对象的边界框),本节介绍了如何使用这两个重要的 OpenCV 功能。

1. OpenCV 基础

1.1 安装 OpenCV

OpenCV 是一个开源库,可以用于开发在 WindowsLinuxAndroidmacOS 等平台上运行的计算机视觉应用程序。自 1999 年推出以来,它已成为计算机视觉领域广泛采用的主要开发工具。在 OpenCV 网站,根据所用计算机的不同平台( Unix/WindowsAndroid) 下载相对应的 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,可以开发能够运行在不同操作系统上的应用程序,例如 WindowsLinuxmacOS
  • 它包含一个完整的跨平台 GUI 库,遵循面向对象和事件驱动模型
  • Qt 还包括多个跨平台库,可用于开发多媒体、数据库、多线程、Web 应用程序以及其他高级应用程序

使用 Qt 可以很方便的编译 OpenCV 库,因为它可以读取 CMake 文件。一旦安装了 OpenCVCMake,只需从 QtFile 菜单中选择 Open File or Project...,然后打开 OpenCV 的源目录下的 CMakeLists.txt 文件,然后在弹出窗口中单击 configure project

OpenCV实战(1)——OpenCV与图像处理

创建一个 OpenCV 项目后,可以通过单击 Qt 菜单中的 Build Project 来构建该项目:

OpenCV实战(1)——OpenCV与图像处理

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实战(1)——OpenCV与图像处理

加载图像后,通常需要对图像进行一些处理。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);

可以看到输出图像显示在一个不同的窗口中,如下图所示:

OpenCV实战(1)——OpenCV与图像处理

2.1.3 保存图像

最后,将处理后的图像保存在磁盘上,可以使用 highgui 库函数完成图像保存操作:

cv::imwrite("output.png", result); // 保存处理结果

文件扩展名决定了将使用哪个编解码器来保存图像,常见的图像格式包括 BMPJPGTIFFPNG 等。

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 命名空间

OpenCVC++ 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) 组成的图像,OpenCVCV_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_16U32 位有符号整数指定为 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_opencvpkg-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 是一个整数,用于指定哪种类型的鼠标事件触发了对回调函数的调用;紧接着的两个参数 xy 是事件发生时鼠标位置的像素坐标;最后一个参数用于以指向对象的指针的形式向函数发送一个额外的参数。可以通过以下方式在应用程序中注册此回调函数:

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_MOVEcv::EVENT_LBUTTONUPcv::EVENT_RBUTTONDOWNcv::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

OpenCV实战(1)——OpenCV与图像处理

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

OpenCV实战(1)——OpenCV与图像处理

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实战(1)——OpenCV与图像处理

小结

OpenCV 是一个跨平台计算机视觉和机器学习库,实现了图像处理和计算机视觉方面的很多通用算法。本文,首先介绍了 OpenCV 的基础知识,并介绍了如何在不同平台安装 OpenCV 库,同时演示了如何使用不同方式编写、编译和执行 OpenCV 应用程序。然后,我们还总结了 OpenCV 处理图像的基础函数和功能,包括读取、显示和保存图像,以及鼠标事件和图像绘制。