Part11. 绘制简单的图形
绘图功能是 OpenCV 最基础的功能,OpenCV 提供了基础的绘制函数,用于帮助我们绘制一些基本的图形。通过这些函数的组合,我们也可以做一些高级的应用。
11.1 绘制点和圆
OpenCV 的绘制函数相对简单,而且很多参数很类似,所以介绍第一个函数时会详细地介绍各个参数的含义,后面就不做特别详细的介绍了。
我们先来看点和圆的绘制:
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
using namespace std;
using namespace cv;
int main(int argc,char *argv[])
{
Mat image = Mat::zeros(Size(800, 800), CV_8UC3);
image.setTo(255);// 设置屏幕为白色
Point p1(100, 100);
Point p2(200, 200);
Point p3(300, 300);
Point p4(400, 400);
Point p5(500, 500);
Point p6(600, 600);
Point p7(700, 700);
circle(image, p1, 4, Scalar(0, 0, 255), -1); // 画半径为4的圆(画点)
circle(image, p2, 60, Scalar(255, 0, 0), 2); // 画半径为60的圆
circle(image, p3, 60, Scalar(0, 255, 0), -1);
circle(image, p4, 60, Scalar(255, 255, 0), 5);
circle(image, p5, 60, Scalar(255, 0, 255), -1);
circle(image, p6, 60, Scalar(0, 255, 255), 2);
circle(image, p7, 60, Scalar(0, 0, 0), -1);
imshow("src", image);
waitKey(0);
return 0;
}
我们主要使用 circle() 函数来绘制点和圆。
CV_EXPORTS_W void circle(InputOutputArray img, Point center, int radius,
const Scalar& color, int thickness = 1,
int lineType = LINE_8, int shift = 0);
其各个参数的含义:
第一个参数 img:输入的源图像。 第二个参数 center:圆心的坐标。 第三个参数 radius:圆的半径。 第四个参数 color:圆形的颜色。 第五个参数 thickness:如果是正数,表示组成圆的线条的粗细程度。如果是负数,表示圆被填充。 第六个参数 lineType:线条的类型。OpenCV 提供了三种类型的线条,它们都是 LineTypes 枚举类型。
LINE_4 :4,表示四连接线。
LINE_8 :8,表示八连接线。
LINE_AA :16,表示抗锯齿线。使用它会产生更好的绘图质量,图像看起来会非常平滑,但是绘制速度较慢。
第七个参数 shift:圆心坐标点和半径值的小数点位数。
这里很多的参数,在本文后续的函数中都会用到。
21.2 绘制直线
直线跟圆的区别是,直线需要2个点来确定位置。下面是绘制直线的例子:
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
using namespace std;
using namespace cv;
int main(int argc,char *argv[])
{
Mat image = Mat::zeros(Size(800, 800), CV_8UC3);
image.setTo(255);// 设置屏幕为白色
Point p1(100, 100);
Point p2(700, 700);
Point p3(700, 100);
Point p4(100, 700);
line(image, p1, p2, Scalar(0, 0, 255), 2);
line(image, p3, p4, Scalar(255, 0, 0), 2);
imshow("src", image);
waitKey(0);
return 0;
}
31.3 绘制矩形
矩形有两种绘制方式,一种是定义好矩形的左上角点位置和矩形长宽,然后在图像上绘制出来;另一种是通过确定矩形的左上角点和右下角点来确定矩形的位置,然后在图像上绘制出来。
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
using namespace std;
using namespace cv;
int main(int argc,char *argv[])
{
Mat image = Mat::zeros(Size(800, 800), CV_8UC3);
image.setTo(255);// 设置屏幕为白色
Rect rect(150, 150, 120, 200);
rectangle(image, rect, Scalar(0, 0, 255), 4);
rectangle(image,Point(200,400), //两个对角点
Point(600,600),
Scalar(255,0,0),
-);
imshow("src", image);
waitKey(0);
return 0;
}
41.4 绘制椭圆
椭圆的绘制稍微复杂一点,除了椭圆的中心位置以外,还需要确定椭圆的旋转角度、横轴长、纵轴长,这样才能绘制出椭圆。
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
using namespace std;
using namespace cv;
int main(int argc,char *argv[])
{
Mat image = Mat::zeros(Size(800, 800), CV_8UC3);
image.setTo(255);// 设置屏幕为白色
Point p1(100, 100);
Point p2(200, 200);
Point p3(300, 300);
Point p4(400, 400);
Point p5(600, 600);
ellipse(image,p1,Size(60, 30),30,0,360,Scalar(255, 255, 0),4);
ellipse(image,p2,Size(30, 60),0,0,360,Scalar(255, 0, 0),-1);
ellipse(image,p3,Size(60, 30),120,0,360,Scalar(0, 255, 0),4);
ellipse(image,p4,Size(100, 100),0,0,360,Scalar(0, 0, 255),-1);
ellipse(image,p5,Size(120, 60),0,0,360,Scalar(255, 0, 255),4);
imshow("src", image);
waitKey(0);
return 0;
}
绘制椭圆的 ellipse() 函数的定义:
CV_EXPORTS_W void ellipse(InputOutputArray img, Point center, Size axes,
double angle, double startAngle, double endAngle,
const Scalar& color, int thickness = 1,
int lineType = LINE_8, int shift = 0);
其中, 第三个参数 axes: Size 的两个参数分别是横轴的长度、纵轴的长度。当横轴和纵轴相等时,那就表示是圆形。 第四个参数 angle:椭圆旋转角度。 第五个参数 startAngle:从主轴顺时针方向测量的椭圆弧的起点。 第六个参数 endAngle:从主轴顺时针方向测量的椭圆弧的终点。当 startAngle 和 endAngle 的值为 0、360 才会绘制完整的椭圆。
51.5 绘制多边形
多面体相对于椭圆更加复杂一些,多面体的绘制本身也有两种函数可以实现。
polylines() 函数根据点集绘制多条相连的线段用以组成多面体,fillPoly() 函数绘制具有填充效果的多面体。因此, thickness 参数是否为负数无法对多面体的填充起作用。
#include <vector>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
using namespace std;
using namespace cv;
int main(int argc,char *argv[])
{
Mat image = Mat::zeros(Size(800, 800), CV_8UC3);
image.setTo(255);// 设置屏幕为白色
Point p1(100, 100);
Point p2(350, 100);
Point p3(450, 280);
Point p4(320, 450);
Point p5(100, 400);
std::vector<Point> pts;
pts.push_back(p1);
pts.push_back(p2);
pts.push_back(p3);
pts.push_back(p4);
pts.push_back(p5);
polylines(image, pts, true, Scalar(255, 0, 255), 4);
Point p6(500, 500);
Point p7(720, 650);
Point p8(650, 780);
Point p9(550, 700);
Point p10(300, 700);
pts.clear();
pts.push_back(p6);
pts.push_back(p7);
pts.push_back(p8);
pts.push_back(p9);
pts.push_back(p10);
fillPoly(image, pts, Scalar(0, 255, 255));
imshow("src", image);
waitKey(0);
return 0;
}
简单介绍一下 polylines() 函数,另一个 fillPoly() 函数很类似。
CV_EXPORTS_W void polylines(InputOutputArray img, InputArrayOfArrays pts,
bool isClosed, const Scalar& color,
int thickness = 1, int lineType = LINE_8, int shift = 0 );
第二个参数 pts: 输入多边形的点的集合。 第三个参数 isClosed:是否把绘制的多条线段首尾相连,如果要绘制成多边形这个参数很重要,需要设置成 true。
61.6 图像中添加文字
OpenCV 提供了在原图上添加文字的 putText() 函数,支持字体、字号的设置。
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
using namespace std;
using namespace cv;
int main(int argc,char *argv[])
{
Mat image = Mat::zeros(Size(800, 800), CV_8UC3);
image.setTo(255);// 设置屏幕为白色
string text = "Hello OpenCV!";
putText(image, text, Point(0,100), FONT_HERSHEY_PLAIN, 2, cv::Scalar(0, 0, 255), 2);
putText(image, text, Point(100,200), FONT_HERSHEY_PLAIN, 4, cv::Scalar(255, 0, 255), 4);
//设置绘制文本的相关参数
int fontFace = cv::FONT_HERSHEY_SIMPLEX;
double fontScale = 2;
int thickness = 8;
int baseline;
// 通过 getTextSize() 函数先获取待绘制文本的大小
Size textSize = getTextSize(text, fontFace, fontScale, thickness, &baseline);
// 计算出文本绘制到图片居中的位置
Point point;
point.x = image.cols / 2 - textSize.width / 2;
point.y = image.rows / 2 + textSize.height / 2;
putText(image, text, point, fontFace, fontScale, cv::Scalar(255, 255, 0), thickness);
imshow("src", image);
waitKey(0);
return 0;
}
Part22. 轮廓入门和绘制轮廓
72.1 轮廓入门
轮廓是机器视觉的常用概念。它是由一系列相连的点组成的曲线,具有相同的颜色或灰度。轮廓常用于形状分析、物体检测、识别等任务。
一般情况下,为了得到精准的轮廓需要先对图像进行二值化处理,例如使用阈值分割或者 Canny 边缘检测等方式得到二值图像。然后,对二值图像进行轮廓发现和轮廓分析。
轮廓发现是利用 findContours() 函数检测图像中的对象边界,将每一个轮廓以点向量方式存储。因此,可以得到一个图像的拓扑信息,包含了一个轮廓的后一个轮廓、前一个轮廓、父轮廓和内嵌轮廓的索引编号。
在获取图像轮廓之后,我们就可以通过轮廓的属性(例如:轮廓的面积、质心、周长、几何矩、中心矩等等)来分析和筛选轮廓。轮廓具有很多属性和性质,我们会在后面的文章详细地介绍更多的内容,本文只是作为简单的入门介绍。
下面的例子,展示了获取手机的轮廓图,并获取其最小外接矩形以及截取 roi:
#include <iostream>
#include <vector>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
using namespace std;
using namespace cv;
bool ascendSort(vector<Point> a,vector<Point> b)
{
return contourArea(a) > contourArea(b);
}
int main(int argc,char *argv[])
{
string fileName = ...;
Mat image = imread(fileName);
if (image.empty()) {
return -1;
}
imshow("src",image);
Mat gray;
cvtColor(image,gray,COLOR_BGR2GRAY);
Mat thresh;
threshold(gray, thresh, 0, 255, THRESH_BINARY | THRESH_OTSU);
// 定义变量轮廓
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(thresh, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);
sort(contours.begin(), contours.end(), ascendSort);//ascending sort
RotatedRect rrt = minAreaRect(contours[0]);
Rect bbox = rrt.boundingRect();
Mat roi;
try {
roi = image(bbox);
} catch (...) {
return -1;
}
imshow("roi",roi);
waitKey(0);
return 0;
}
上述代码,首先将原图转换成灰度图像,再进行阈值分割变成二值图像。然后,对轮廓进行查找并按照轮廓面积的大小进行排序。最后,对最大的轮廓获取其最小外接矩形以及截取这个最小外接矩形作为 roi,并将其展示。
在这段代码中,有些函数的作用和解释会在以后的小节中详细介绍。本文只详细解释如何进行轮廓发现和查找,主要使用的是 findContours() 函数。它包含很多参数,我们有必要简介绍一下各个参数的含义。
第一个参数 image: 输入的源图像。一个 CV_8UC1 的单通道图像。 第二个参数 contours: 输出轮廓图像。每个轮廓都存储为点向量 std::vector< cv::Point >,由多个轮廓组成输出的全部轮廓 std::vector<std::vector < cv::Point >>。 第三个参数 hierarchy:输出各个轮廓的继承关系。是 std::vector < cv::Vec4i > 类型的向量,长度跟 contours 的长度一致,每个元素和 contours 的元素对应,包含了有关图像拓扑的信息。 第四个参数 mode:轮廓检测的模式。包括以下四种:
RETR_EXTERNAL:只检测外轮廓,忽略轮廓内部的洞。
RETR_LIST:检测所有的轮廓,但不建立继承(包含)关系。
RETR_TREE:检测所有的轮廓,并且建立所有的继承(包含)关系。
RETR_CCOMP:检测所有轮廓,但是仅仅建立两层包含关系。
RETR_FLOODFILL:洪水填充法。采用这种模式时,输入的源图像也可以是 32 位的整型图像(CV_32SC1)。
第五个参数 method:每个轮廓的编码信息。包括以下四种:
CHAIN_APPROX_NONE:把轮廓上所有的点存储。
CHAIN_APPROX_SIMPLE:只存储轮廓上的拐点。
CHAIN_APPROX_TC89_L1:使用 teh-Chinl chain 近似算法。
CHAIN_APPROX_TC89_KCOS 使用 teh-Chinl chain 近似算法。
第六个参数 offset:每个轮廓点移动的偏移量。表示所有的轮廓信息相对于原始图像的偏移量,它是一个可选参数,cv::Point()类型。
82.2 绘制轮廓
在上述的代码找到了轮廓之后,绘制轮廓就变得很简单了。我们使用 drawContours() 函数就可以绘制轮廓。
drawContours(image,contours,0,Scalar(0,0,255),8);
imshow("contours",image);
再结合 imshow() 函数,可以直接在原图上展示绘制出来的手机轮廓。
drawContours() 函数的参数就不一一解释了,我们只解释2个参数的含义。 第二个参数 contours: 输入全部的轮廓图像。 第三个参数 contourIdx:轮廓索引号,从 0 开始。-1 表示绘制所有轮廓。
通过这个函数,我们学会了绘制轮廓。在调试代码的时候,我经常会在原图上绘制一下查找到的相关轮廓,看看查找的内容是否准确。
Part33. 总结
本文主要分成两个部分。第一部分介绍了 OpenCV 基本的绘制函数以及使用,它们的使用比较简单只要明白每个函数中各个参数的含义即可。如果将这些函数组合起来使用,也可以做一些相对高级的应用。
第二部分介绍了轮廓的入门知识,主要是轮廓发现和轮廓绘制。轮廓是图像处理的核心内容之一,它包含了很多重要的信息和性质,我们会在后面的文章中重点学习。