目录
- 1、前言
- 2、基本绘图函数
- 3、原子图绘制
- 4、多边形绘制+最小外接矩形
- 5、鼠标绘图+最小外接矩形
1、前言
图像处理中经常用到基本图形的绘制,比如直线、圆、矩形,在上一文中在直方图绘制中使用了OpenCV的line()函数来绘制直方图,不仅如此,基本图形在很多大型项目中也会频频使用,比如物体识别中,就需要绘制矩形来框选物体所在区域作为候选区,方便后续特征识别处理,本文通过介绍OpenCV基本绘图函数,如line()、Rectangle()等,来完成原子图绘制,另外拓展两个小demo
- 绘制任意多边形并求最小外接矩形
- 鼠标控制任意图形绘制并求最小外接矩形
通过本文学习可以熟悉OpenCV基本图形绘制、图形轮廓最小外接矩形、鼠标响应与回调函数。
2、基本绘图函数
OpenCV绘图函数 | 作用 |
line() | 直线绘制 |
rectangle() | 矩形绘制 |
ellipse() | 椭圆绘制 |
circle() | 圆形绘制 |
filllines() | 多边形绘制 |
fillPoly() | 填充多边形绘制 |
OpenCV中直线绘制的函数是line,其函数原型如下:
void line(Mat& img, //需绘制线段的图像
Point pt1, //直线起点
Point pt2, //直线终点
const Scalar& color, //直线的颜色,Scalar类定义
int thickness=1, //线宽,默认为1
int lineType=8, //线型,可以取值8、4和CV_AA,分别代表8邻接连接线,4邻接连接线和反锯齿连接线。
int shift=0 //坐标小数点位数,不用管
)
【注】绘制线段图像img,不能设为const Mat型,因为绘制过程需要修改img像素值,之前因为这个总是找不到error;线型lineType不是表示工程上的虚线、点划线等,而是像素点连接方式,一般取8-邻域连接方式,具体可参考相关Blog。
OpenCV中矩形绘制的函数是rectangle,其函数原型如下:
void rectangle(InputOutputArray img, //需绘制矩形的图像
Point pt1, //矩形的一个顶点
Point pt2, //矩形的另一个顶点
const Scalar &color, //矩形线条颜色
int thickness = 1, //线宽,默认为1
int lineType = 8, //线型,默认8-邻域
int shift = 0 //坐标小数点位数,不用管
)
//重载rectangle定义
void rectangle(Mat &img, //需绘制矩形的图像
Rect rec, //Rect类定义的矩形
const Scalar &color, //矩形线条颜色
int thickness = 1, //线宽,默认为1
int lineType = 8, //线型,默认8-邻域
int shift = 0 //坐标小数点位数,不用管
)
【注】笔者在coding过程中发现rectangle原型有两种定义方式,一是通过两个顶点定义矩形的形状,二是通过Rect类定义一个矩形,对于之前定义过的rectangle,可以直接调用快速绘制。
OpenCV中椭圆绘制的函数是ellipse,其函数原型如下:
void ellipse(Mat&img, //需绘制椭圆的图像
Point center, //椭圆中心点
Size axes, //轴的长度,Size类型
double angle, //椭圆倾斜的角度,0度时放正
double startAngle, //椭圆圆弧起始角度
double endAngle, //椭圆圆弧终止角度
const Scalar&color, //线的颜色
intthickness=1, //线宽,默认为1
int lineType=8, //线型,默认8-邻域
intshift=0 //坐标小数点位数,不用管
)
//重载ellipse定义
void ellipse(Mat& img, //需绘制椭圆的图像
const RotatedRect& box, //椭圆外接矩形
const Scalar& color, //线的颜色
int thickness=1, //线宽,默认为1
intlineType=8 //线型,默认8-邻域
)
【注】在ellipse()函数重载定义中,可以通过椭圆外接矩形的方式绘制唯一一个椭圆,因为一个椭圆只有一个最小外接矩形,这个在后面的demo中也有涉及。
OpenCV中圆形绘制的函数是circle,其函数原型如下:
void circle(InputOutputArray img, //需绘制圆形的图像
cv::Point center, //圆形中心点
int radius, //半径
const Scalar &color, //线的颜色
int thickness = 1, //线宽,默认为1
int lineType = 8, //线型,默认8-邻域
int shift = 0 //坐标小数点位数,不用管
)
【注】线宽thickness定义为-1时,表示圆形内部被填充且填充颜色与线的颜色一致。
OpenCV中多边形绘制的函数是filllines,其函数原型如下:
void polylines(InputOutputArray img, //需绘制多边形的图像
InputArrayOfArrays pts, //多边形顶点向量
bool isClosed, //多边形是否闭合
const Scalar &color, //线的颜色
int thickness = 1, //线宽,默认为1
int lineType = 8, //线型,默认8-邻域
int shift = 0 //坐标小数点位数,不用管
)
//重载polylines定义
void polylines(Mat &img, //需绘制多边形的图像
const Point *const *pts, //多边形顶点集,Point数组表示
const int *npts, //多边形顶点数目
int ncontours, //多边形边数
bool isClosed, //多边形是否闭合
const Scalar &color, //线的颜色
int thickness = 1, //线宽,默认为1
int lineType = 8, //线型,默认8-邻域
int shift = 0 //坐标小数点位数,不用管
)
一般用vector<vector>类型的二维点阵表示多边形顶点向量,它涵盖了多边形顶点、边和数量。另外当特别要求多边形填充时,会用到另一个多边形函数fillPoly,虽然和polylines差不多,但在这里还是写出来,也方便以后查询。其函数原型如下:
void fillPoly(InputOutputArray img, //需绘制和填充多边形的图像
InputArrayOfArrays pts, //多边形顶点向量
const cv::Scalar &color, //多边形填充颜色
int lineType = 8, //线型,默认8-邻域
int shift = 0, //坐标小数点位数,不用管
Point offset = cv::Point() //平移点
)
//重载fillPoly定义
void fillPoly(Mat &img, //需绘制和填充多边形的图像
const Point **pts, //多边形顶点集,Point数组表示
const int *npts, //多边形顶点数目
int ncontours, //多边形边数
const cv::Scalar &color, //多边形填充颜色
int lineType = 8, //线型,默认8-邻域
int shift = 0, //坐标小数点位数,不用管
Point offset = cv::Point() //平移点
)
绘制实心目标时,一般将thickness设为-1即可,但是线颜色和填充颜色一致,多边形填充的填充颜色可任意设置。
3、原子图绘制
参考《OpenCV3编程入门》,以绘制原子图模型为例,展示一下OpenCV中基本绘图函数的使用,代码如下:
#include<opencv2/opencv.hpp>
#define WIDTH 600 //宏定义显示窗口大小
using namespace cv;
void Draw_Ellipse(const Mat& image,const double angle){
int thickness=2;
int lineType=8;
ellipse(image,Point(WIDTH/2,WIDTH/2),Size(WIDTH/4,WIDTH/16),angle,0,360,Scalar(255,0,0),thickness,lineType);
}
void Draw_centerCircle(const Mat& image,Point center){
int thickness=-1;
int lineType=8;
circle(image,center,WIDTH/32,Scalar(0,0,255),thickness,lineType);
}
void Draw_line(const Mat& image,Point start,Point end){
int thickness=2;
int lineType=8;
line(image,start,end,Scalar(0,255,0),thickness,lineType);
}
int main()
{
//1、基本图像绘制
//2、多变形最小外接矩形
//3、鼠标控制基本图形绘制+求外接矩形
Mat atomImage=Mat::zeros(WIDTH,WIDTH,CV_8UC3); //绘制空白Mat画板
//绘制中心线
Draw_line(atomImage,Point(WIDTH/2+WIDTH*3/8,WIDTH/2),Point(WIDTH/2-WIDTH*3/8,WIDTH/2));
Draw_line(atomImage,Point(WIDTH/2,WIDTH/2+WIDTH*3/8),Point(WIDTH/2,WIDTH/2-WIDTH*3/8));
//绘制椭圆
Draw_Ellipse(atomImage,0);
Draw_Ellipse(atomImage,45);
Draw_Ellipse(atomImage,90);
Draw_Ellipse(atomImage,135);
//绘制圆心
Draw_centerCircle(atomImage,Point(WIDTH/2,WIDTH/2));
imshow("AtomImage",atomImage);
waitKey();
return 0;
}
效果如下:
4、多边形绘制+最小外接矩形
#include<iostream>
#include<vector>
#include<opencv2/opencv.hpp>
#define WIDTH 600 //宏定义显示窗口大小
using namespace cv;
using namespace std;
void Draw_polygon(Mat& image){
int lineType=8;
//创建多边形点: 可自行创建
Point point[1][5]; //二维数组表示顶点集
point[0][0]=Point(115,220);
point[0][1]=Point(460,125);
point[0][2]=Point(566,350);
point[0][3]=Point(435,365);
point[0][4]=Point(310,510);
const Point* ppt=point[0]; //point[0]地址作为顶点集首地址
int npt=5; //顶点个数
polylines(image, &ppt, &npt, 1, 1, Scalar(0,0255),1,8,0); //绘制多边形不填充
//fillPoly(image,&ppt,&npt,1,Scalar(255,0,0),lineType); //绘制多边形填充
}
int main()
{
//绘制多边形 + 外接矩形
Mat polyImage=Mat::zeros(WIDTH,WIDTH,CV_8UC3); //绘制空白Mat画板
Draw_polygon(polyImage);
Mat poly_gray;
cvtColor(polyImage,poly_gray,CV_RGB2GRAY); //灰度处理
vector<vector<Point>>contours; //轮廓(二维点集)
vector<Vec4i> hierarchy;
vector<RotatedRect> rect; //rect最小旋转矩形
findContours(poly_gray, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE); //查找轮廓
for (int i = 0; i < contours.size(); i++) //绘制最小外接矩形
{
rect.push_back(minAreaRect(contours[i]));
Point2f vertices[4]; //定义矩形的4个顶点
rect[i].points(vertices); //计算矩形的4个顶点
for (int i = 0; i < 4; i++)
line(polyImage, vertices[i], vertices[(i + 1) % 4], Scalar(255, 255, 255),1);
cout <<"width is:"<<rect[i].size.width << endl;
cout << "height is:" << rect[i].size.height << endl;
}
imshow("polyImage",polyImage);
waitKey();
return 0;
}
因为vector<RotatedRect>类型可以自动寻找最小外接矩形,而多边形的最小外接矩形只有一个,所以可以表示出来。效果如下:
5、鼠标绘图+最小外接矩形
OpenCV中进行鼠标操作主要用到setMouseCallback这个函数,函数原型如下:
void setMouseCallback(const String& winname, //鼠标响应窗口名
MouseCallback onMouse, //鼠标响应函数,通过回调函数处理鼠标事件
void* userdata = 0 //用户自定义的参数
);
主要操作在回调函数onMouse中设置:
void on_Mouse(int event, //表示鼠标事件类型的常量
int x, //鼠标指针在图像坐标系的横坐标
int y, //鼠标指针在图像坐标系的纵坐标
int flags, //鼠标事件标志的常量
void* param //用户可自定义的参数
);
鼠标事件Event的类型(字母和数字完全等价):
#define CV_EVENT_MOUSEMOVE 0 //滑动
#define CV_EVENT_LBUTTONDOWN 1 //左键点击
#define CV_EVENT_RBUTTONDOWN 2 //右键点击
#define CV_EVENT_MBUTTONDOWN 3 //中键点击
#define CV_EVENT_LBUTTONUP 4 //左键放开
#define CV_EVENT_RBUTTONUP 5 //右键放开
#define CV_EVENT_MBUTTONUP 6 //中键放开
#define CV_EVENT_LBUTTONDBLCLK 7 //左键双击
#define CV_EVENT_RBUTTONDBLCLK 8 //右键双击
#define CV_EVENT_MBUTTONDBLCLK 9 //中键双击
这里的鼠标响应函数onMouse和混动条(Trackbar)回调函数一样,
需要进行的操作全在回调函数onMouse里面进行。
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
由于回调函数在main函数体外定义,所以回调函数内部用到的变量需全局定义,当参数量不大的时候可以设置一些外部全局变量,但是这就使得程序内存开销增大,可以通过用户自定义结构体或类来实现体内传参调用,就避免了额外定义全局变量。
代码如下:
#include<iostream>
#include<vector>
#include<opencv2/opencv.hpp>
#define WIDTH 600 //宏定义显示窗口大小
using namespace cv;
using namespace std;
void Draw_circumRect(Mat& src,Mat& grayImage){
vector<vector<Point>>contours;
vector<Vec4i> hierarchy;
vector<RotatedRect>rect;
//【5】查找轮廓
findContours(grayImage, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
for (int i = 0; i < contours.size(); i++)
{
rect.push_back(minAreaRect(contours[i]));
Point2f vertices[4]; //定义矩形的4个顶点
rect[i].points(vertices); //计算矩形的4个顶点
for (int i = 0; i < 4; i++)
line(src, vertices[i], vertices[(i + 1) % 4], Scalar(255, 255, 255),1);
cout <<"width的值:"<<rect[i].size.width << endl;
cout << "height的值:" << rect[i].size.height << endl;//其实只有一个外接矩形
}
}
int point_num=0;
Point P0,P1,P2;
Mat img,newImg,grayImg;
void on_Mouse(int event,int x,int y,int flag,void* para){
if (event == CV_EVENT_LBUTTONDOWN){
P0=Point(x,y);
P1=Point(x,y); //鼠标按下作为起始点
}
else if ((event == CV_EVENT_MOUSEMOVE) && (flag==1))//鼠标按下并且光标移动
{
P2=Point(x,y);
//从起点连线
line(img,P1,P2,Scalar(255,255,255),3,8);
P1=P2;
point_num++;
}
else if (event == 4){
line(img,P0,P1,Scalar(255,255,255),3,8);
//point_num=0; //记录每次绘制Point数量
img.copyTo(newImg);
cvtColor(newImg,grayImg,CV_RGB2GRAY);
Draw_circumRect(newImg,grayImg);
imshow("circum_Rect",newImg);
}
}
int main()
{
int key=0;
img=Mat::zeros(WIDTH,WIDTH,CV_8UC3);
namedWindow("mouse_Graph"); //鼠标响应窗口
setMouseCallback("mouse_Graph",on_Mouse); //鼠标回调函数
while(1){
imshow("mouse_Graph", img);
key=waitKey(30);
if (key == 27){ //按下ESC退出整个程序,保存视频文件到磁盘
break;
}
}
waitKey();
return 0;
}
效果如下:
当然也可以自己加载一幅图像,灰度化后进行阈值分割,得到物体边缘,最后用一个最小矩形框框出物体或ROI区域,其实上面的过程就是物体识别过程中得到候选框的过程,这只适用物体之间无交叉的情况,对于多物体检测可以参考R-CNN、Fast R-CNN等,它们在获取候选框的时候还是有很多经典的方法的,比如划窗法、RPN等。后面再更新~