OpenCV计算机视觉开发实践:基于Qt C++ - 商品搜索 - 京东
图像轮廓可以简单地解释为连接具有相同颜色(在彩色图片中)或强度(灰度图像要转换为二值化图像)的所有连续点(沿边界)的曲线。值得注意的是,图像轮廓指的是图像中连续的像素边界,这些边界通常代表了图像中的物体或者物体的边缘。轮廓是由相同像素值组成的曲线,它们连接相同的颜色或灰度值,并且具有连续性。
这里要强调一下,在OpenCV中,查找轮廓就像从黑色背景中找到白色物体,如图17-1所示。
图17-1
对于左图中白色区域的图像,经过程序处理找出轮廓并绘制出来,就得到了右边的轮廓。注意,要找的对象应该是白色,背景应该是黑色。为了获得更高的准确性,要使用二值图像。
17.2 应用场景
轮廓可以用来描述和分析图像中的形状和结构,因此是许多计算机视觉任务(如目标检测、形状识别、图像分割等)的基础。图像轮廓在许多应用场景中都发挥着重要作用,下面列举一些常见的应用场景:
(1)目标检测与识别:轮廓可以用于检测和定位图像中的物体。通过检测物体的轮廓,可以识别出图像中的不同物体并进行分类。
(2)图像分割:轮廓可以用来分割图像中的不同区域或物体。通过提取物体的轮廓,可以将图像分成多个不同的部分,以方便进一步分析和处理。
(3)医学图像分析:在医学图像中,轮廓可以用来标记器官、病变或细胞等结构。这对于诊断和治疗决策具有重要意义。
(4)工业自动化:在工业自动化中,轮廓可用于检测产品的缺陷、测量尺寸和定位部件,从而实现自动化生产和质量控制。
(5)机器人视觉:机器人可以利用图像轮廓来感知环境和物体,从而实现自主导航、抓取物体等任务。
(6)计算机辅助设计(CAD):在CAD领域,图像轮廓可用于从实际物体中获取几何信息,以便在计算机上进行建模和设计。
(7)虚拟现实与增强现实:图像轮廓可以用来实时跟踪物体,将虚拟对象与实际场景进行交互,从而创建更加逼真的虚拟现实或增强现实体验。
(8)图像重建与三维建模:利用物体的轮廓可以进行图像的重建和三维建模,从而生成立体的物体模型。
(9)边缘检测虽然能够检测出边缘,但边缘是不连续的,检测到的边缘并不是一个整体。图像轮廓是指将边缘连接起来形成的一个整体,可用于后续的计算。
图像轮廓是图像中非常重要的一个特征信息,通过对图像轮廓的操作,我们能够获取目标图像的大小、位置、方向等信息。
17.3 OpenCV中的轮廓函数
OpenCV提供了查找图像轮廓的函数findContours,该函数能够查找图像内的轮廓信息,其使用形式如下:
void cv::findContours(InputOutputArray image, OutputArrayOfArrays contours, OutputArray hierarchy, int mode, int method, Point offset=Point());
该函数在4.7节已经介绍过了,这里不再赘述。findContours函数没有返回值(即返回类型为void),但它可以通过修改传入的contours和(可选的)hierarchy参数来输出轮廓和层级信息。
之前提到轮廓是一个具有相同灰度值的边界,它会存储形状边界上所有的(x,y)坐标。实际上我们不需要所有的点,比如当需要直线时,找到两个端点即可。对此可以使用CHAIN_APPROX_SIMPLE,它会将轮廓上的冗余点去掉,压缩轮廓,从而节省内存开支。下面用矩阵来演示,在轮廓列表中的每一个坐标上画一个蓝色圆圈。第一幅图是使用CHAIN_APPROX_NONE的效果,一共734个点;第二幅图是使用CHAIN_APPROX_SIMPLE的结果,只有4个点,如图17-2所示。
图17-2
左图因为有734个点,所以看起来像围着矩形画了一圈线,而右图才4个点。
轮廓具有4个基本属性,通过这些属性,我们可以更好地了解图像轮廓的基本信息。这4个基本属性具体如下:
1)轮廓的个数
使用如下语句可以获取轮廓的个数:
cout << contours.size();
2)每个轮廓的点数
每一个轮廓都是由若干个像素点构成的,点的个数不固定,具体个数取决于轮廓的形状。
例如,使用如下语句可以获取某个轮廓内点的个数:
cout<<contours[i].size); // 打印第i个轮廓的长度(点的个数)
3)轮廓内的点
使用如下语句可以获取第i个轮廓中具体点的位置:
cout<<contours[i]; // 打印第i个轮廓中的像素点
4)轮廓层次
hierarchy可以用来表示图像的拓扑信息(轮廓层次)。图像内的轮廓可能位于不同的位置,比如一个轮廓在另一个轮廓的内部。在这种情况下,我们将外部的轮廓称为父轮廓,内部的轮廓称为子轮廓。按照上述关系分类,一幅图像中所有轮廓之间就建立了父子关系。根据轮廓之间的关系,就能够确定一个轮廓与其他轮廓是如何连接的。比如,确定一个轮廓是某个轮廓的子轮廓,或者是某个轮廓的父轮廓。上述关系被称为层次(组织结构),返回值hierarchy就包含上述层次关系。每个轮廓的contours[i]对应4个元素来说明当前轮廓的层次关系,其形式为:
[Next,Previous,First_Child,Parent]
Next表示后一个轮廓的索引编号;Previous表示前一个轮廓的索引编号;First_Child表示第1个子轮廓的索引编号;Parent表示父轮廓的索引编号。
如果上述各个参数所对应的关系为空,也就是没有对应的关系,则将该参数所对应的值设为“-1”。使用语句“cout<<hierarchy”来查看hierarchy的值。需要注意,轮廓的层次结构是由参数mode决定的。也就是说,使用不同的mode,得到轮廓的编号是不一样的,得到的hierarchy也不一样。
通常光找到轮廓是不够的,一般要将轮廓标记出来,也就是绘制轮廓,这样就可以一目了然了。OpenCV提供了绘制轮廓函数drawContours,其原型如下:
void cv::drawContours(InputOutputArray image, InputArrayOfArrays contours,
int contourIdx, const Scalar& color,
int thickness = 1, int lineType = LINE_8,
InputArray hierarchy = noArray(),
int maxLevel = INT_MAX, Point offset = Point());
这个函数在4.7节已经介绍过了,这里不再赘述。我们把函数原型写在这里,也是为了方便读者就近查阅。
17.3.4 求轮廓面积contourArea
除了绘制轮廓外,OpenCV 还提供了函数contourArea,用于计算轮廓的面积。所谓轮廓面积就是轮廓包围起来的区域的面积,如图17-3所示。3个几何图形的白色区域的面积都是轮廓包围起来的面积。
图17-3
函数contourArea对于分析图像中的对象或区域非常有用,其原型如下:
double cv::contourArea( InputArray _contour, bool oriented );
其中参数contour表示这是一个轮廓的点集,通常通过findContours函数获得;oriented(可选)表示如果提供了这个参数,就返回有方向的面积,0表示顺时针方向,正数表示逆时针方向。函数的返回值是轮廓的面积。
17.4 实战轮廓函数
本节将通过实例来演示findContours、drawContours、contourArea这3个函数的使用。
【例17.1】查找和绘制轮廓,并计算面积
打开Qt Creator,新建一个控制台项目,项目名称是test。
在main.cpp中输入如下代码:
#include "opencv2/opencv.hpp"
using namespace cv;
#include <iostream>
#include <vector>
#include <QDebug>
using namespace std;
int main(int argc, char** argv)
{
Mat thresh,gray;
vector<std::vector<cv::Point>> contours;
vector<cv::Vec4i> hierarchy;
int i;
// 加载图像并转换为灰度图
Mat image = imread("test17.jpg");
cvtColor(image,gray, COLOR_BGR2GRAY);
// 二值化处理
threshold(gray,thresh, 127, 255, THRESH_BINARY);
imshow("bin", thresh);
// 查找所有图形的轮廓
findContours(thresh,contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);
for ( i = 0; i < contours.size(); ++i) { // 遍历所有轮廓
qDebug()<<"第"<<i<<"个轮廓的面积为"<<contourArea(contours[i])<<",长度为"<<arcLength(contours[i], true);
drawContours(image, contours, i, Scalar(0, 0, 255), 3);
}
// 查看轮廓的属性
qDebug()<< "轮廓集合的数据类型: " << typeid(contours).name()<<endl;
qDebug()<<"轮廓的数量:"<<contours.size(); // 轮廓的个数
// 获取并打印数据类型名称,获取轮廓 contours 中第0个元素的类型
qDebug() << "第0个轮廓内的第0个点的数据类型: " << typeid(contours[0][0]).name()<<endl;
size_t count = contours[0].size();// 获取第0个轮廓内点的个数
qDebug() << "第0个轮廓有" << count << "个点" << endl;
cout<<contours[0]; // 打印第0个轮廓中的像素点的位置
imshow("Contours", image);// 显示绘制轮廓的图片
waitKey(0);// 等待用户按键
destroyAllWindows();// 销毁所有窗口
return 0;
}
在上述代码中,首先读取工程目录下的图片文件test17.jpg,并将其转换为灰度图。然后通过函数threshold进行二值化处理并显示二值图像,注意函数threshold有两个返回值,一个是得到的阈值,另外一个是阈值化后的图像,现在第一个返回值用下画线来忽略掉了,因为后面不需要。二值化处理后,就可以通过findContours来查找轮廓了,找到轮廓后,我们通过一个循环来计算每个轮廓的面积,并调用函数drawContours绘制轮廓。最后打印第0个轮廓的各个属性和绘制轮廓后的图像。
在程序中,我们通过C++运算符获取了轮廓集合和第0个轮廓第0个点的数据类型,typeid的结果可能因编译器而异,并且不同编译器可能给出不同的名称表示。
运行程序,在Qt Creator输出窗口的输出结果如下:
第 0 个轮廓的面积为 2069.5 ,长度为 181.414
第 1 个轮廓的面积为 3063.5 ,长度为 269.238
第 2 个轮廓的面积为 1893 ,长度为 163.196
轮廓集合的数据类型: St6vectorIS_IN2cv6Point_IiEESaIS2_EESaIS4_EE
轮廓的数量: 3
第0个轮廓内的第0个点的数据类型: N2cv6Point_IiEE
第0个轮廓有 5 个点
[25, 101;
25, 146;
71, 146;
71, 102;
70, 101]
二值图像和绘制后的轮廓图的输出结果如图17-4所示。
图17-4
从结果中可以看出,我们在图中找到了3个轮廓,并计算出来这3个轮廓的面积。
我们再来看另外一个手势图文件,比如加载构建目录下的test3.jpg文件,并把打印轮廓0的像素点的那一行代码注释掉,再来运行程序,结果如下:
number of contours: 1
只有一个轮廓,符合预期,运行效果图如图17-5所示。
到这里一切都很顺利,但如果我们让程序读入图17-6所示的图像(indexFinger.jpg),会有几个轮廓呢?
读者可以先想一想,估计很多人脱口而出1个轮廓。非也,答案是2个轮廓。我们把这个图片文件命名为test2.jpg,然后放到项目目录下,再运行程序,结果如下:
number of contours: 2
为什么是2呢?这是因为在OpenCV中,找轮廓就像从黑色背景中找到白色物体。现在图17-6的背景是白色,手是黑色,那相当于白色区域是要找的物体,黑色区域边缘和图片边缘是白色区域的轮廓,而且这两个轮廓是分离的,因此轮廓是2个。我们最后来看一下效果图,如图17-7所示。
图17-5 图17-6 图17-7
图中果然绘制了2个轮廓。那怎么让手势只有一个轮廓呢?很简单,黑白对调即可。也就是让手势区域是白色,其他区域是黑色。下面来讲解图像处理中的黑白翻转。
17.5 实战黑白翻转
在图像处理领域,黑白翻转是一种简单但常用的图像处理技术,通过将图像中的像素点的灰度值反转来实现。这种技术不仅可以用于艺术创作,还可以用于图像增强和特效处理等应用。
在图像处理中,每个像素点都有一个灰度值,通常表示为0~255的一个整数。黑白翻转就是将每个像素点的灰度值取反,即用255减去当前的灰度值。这样就可以实现黑色变为白色,白色变为黑色的效果。
知道原理后,下面看一个示例,演示如何使用OpenCV库来实现黑白翻转操作。
【例17.2】实现图片黑白翻转
打开Qt Creator,新建一个控制台项目,项目名称是test。
在main.cpp中输入如下代码:
#include "opencv2/opencv.hpp"
using namespace cv;
int main(int argc, char** argv)
{
Mat img,img_inverted;
img = imread("indexFinger.jpg", IMREAD_GRAYSCALE);// 灰度模式读取图片
img_inverted = 255 - img; // 黑白翻转
// 显示原图和翻转后的图像
imshow("Original Image", img); // 显示原图
imshow("Inverted Image", img_inverted); // 显示翻转后的图像
imwrite("testRes.jpg", img_inverted); // 保存黑白翻转后图像文件
waitKey(0);
destroyAllWindows();
return 0;
}
在这段代码中,首先使用imread函数读取一幅灰度图像(注意第二个参数是IMREAD_GRAYSCALE,即0),并将其赋值给img变量。然后通过简单的计算255-img实现黑白翻转操作,并将结果保存在img_inverted变量中。接着使用imshow函数显示原图和翻转后的图像,并通过imwrite函数保存翻转后的黑白图像文件。最后通过waitKey(0)等待用户按下任意键后关闭窗口。
运行项目,翻转后的图像如图17-8所示。
图17-8
这里提醒读者注意,以后的工作中,一定注意要绘制轮廓的目标区域的颜色是白色,背景是黑色,不要弄反了!