注:本解决方案使用OpenCV4和OpenCV3实现过,写有非常详细的注释,现在贴出的是OpenCV4的代码,需要在OpenCV3中运行需要更改HOUGH_GRADIENT、COLOR_BGR2GRAY等宏定义,这些宏定义在OpenCV4中被更新。希望本工程能带给大家在形状识别上提供一些思路。
简介:本解决方案应用于机械臂拧内六角螺丝等需要内六角螺丝内六边形信息的项目,本解决方案可以提供内六角螺丝钉的信息,包括六个定点坐标,每条边的方程,每条边的边长,螺丝钉直径半径等。本解决方案消耗资源极低,经测试,一副250*250的图像跑完整个算法流程仅需要10ms
本方面技术方案包括以下几个部分:
1. 物体定位:使用霍夫圆变换确定图像中螺丝钉的具体位置、半径和圆心信息。
2. 图像预处理:将原RGB三通道图像压缩成灰度图,并调整图像至合适的对比度。
3. 图形拟合:
(1)进行轮廓检索,获得其边缘点集。
(2)使用approxPolyDP函数对点集进行拟合。
(3)螺丝正六边形形状大约处于半径的1/2处,所以关键点集应符合:点到圆心的距离在半径的1/3到2/3之间。
(4)将两条直线的角点也设为关键点,避免拟合后的关键点不是角点。如图
若有关键点a、b、c、d,这他们连线的角点k也是关键点,实现代码如下
/********************取交点********************************************************/
for (int i = 0; i < src_point.size() - 2; i++) /*拓展关键点*/
{
key_point.push_back(key_line[i] & key_line[i + 2]); /*直线类&操作已重载,返回直线交点*/
}
key_point.push_back(key_line[src_point.size() - 1] & key_line[1]); /*倒数第一条线和第二条线的角点*/
key_point.push_back(key_line[src_point.size() - 2] & key_line[0]); /*倒数第二条线和第一条线的角点*/
(5)对通过筛选的每一个关键点都拟合一个正六边形,拟合方法如下:
证六边形的两个连续角点和中心为一个等边三角形,即通过中心和边上任意一点可得正六边形的递推关系,如图
解方程组
获得正六边形下一个点坐标:
代码实现如下
Point2d Hexagon::nextPoint(Point2d p0, Point2d p1)
{
double x0 = p0.x;
double y0 = p0.y;
double x1 = p1.x;
double y1 = p1.y;
double x = (x0*x0 - x1*x1 + y0*y0 - y1*y1 - (y0 - y1)*
(y0 + y1 + pow(3., 0.5)*(x0 - x1))) / (2.*(x0 - x1)); /*matlab算出的正六边形递推公司*/
double y = 0.5*(y0 + y1 + (x0-x1)*pow(3., 0.5)); /*matlab算出的正六边形递推公司*/
return Point2d(x,y);
}
连续使用5次递推公式,即可得到一个正六边形。
4. 误差分析
本方案中,误差计算采用如下方案。
(1)角点误差:拟合出来的正六边形的角点和所有的关键点的距离,取最小值为该角点误差。
vector<int> err(6); /*误差容器*/
for (int i = 0; i < 6; i++) /*遍历六边形6个理论角点*/
{
int err_tmp1 = INT_MAX; /*理论角点和关键点的最小误差*/
int index = -1; /*关键点的下标*/
int x2 = vertex[i].x;
int y2 = vertex[i].y;
for (int j = 0; j < points.size(); j++) /*遍历所有关键点*/
{
int x1 = points[j].x;
int y1 = points[j].y;
int err_tmp2 = (x2 - x1)*(x2 - x1) + (y2 - y1)*(y2 - y1); /*计算关键点和角点的误差*/
if (err_tmp2 < err_tmp1) /*取最小误差作为角点和关键点误差*/
{
err_tmp1 = err_tmp2;
index = j;
}
}
points.erase(points.begin() + index); /*移除该关键点,避免一个关键点匹配多个角点的情况*/
err[i] = err_tmp1; /*获得误差*/
}
(2)拟合误差:取拟合的正六边形的前三个角点误差的和为拟合误差。
int err_all = 0; /*该正六边形的误差*/
sort(err.begin(), err.end()); /*排序所有角点误差*/
if (err.size() > 3) /*必须有4个点*/
{
err_all = abs(err[0]) + abs(err[1]) + abs(err[2]); /*取前3,返回误差为前三误差和*/
return err_all;
}
(3)最终误差:每一个关键点都会拟合出一个正六边形,所有关键点拟合出来的拟合误差的取最小值为最终误差。
效果图
工程源代码
main.cpp
#include <iostream>
#include <opencv2/opencv.hpp>
#include "line.h"
#include "hexagon.h"
using namespace std;
using namespace cv;
/********************************************************************************/
/* 功能 : 角度转换成弧度 */
/* 入参 : DEG :角度 */
/* 出参 : 无 */
/* 返回值 : 弧度 */
/********************************************************************************/
double DEG2RAD(double DEG)
{
double PI = 3.1415926535897932384626433832795;
double RAD = DEG / 180. * PI;
return RAD;
}
/********************************************************************************/
/* 功能 : 弧度转换成角度 */
/* 入参 : RAD :弧度 */
/* 出参 : 无 */
/* 返回值 : 角度 */
/********************************************************************************/
double RAD2DEG(double RAD)
{
double PI = 3.1415926535897932384626433832795;
double DEG = RAD / PI * 180.;
return DEG;
}
/********************************************************************************/
/* 功能 : 根据点集生成关键直线 */
/* 入参 : src_point :关键点集 */
/* 出参 : key_point :增加了关键点的点集 */
/* key_line :关键直线 */
/* 返回值 : 是否转换成功 */
/********************************************************************************/
bool getKeyPoint_Line(vector<Point> src_point,vector<Point>&key_point,
vector<Line>&key_line)
{
if (src_point.size() < 3) /*小于三个关键点直接返回*/
{
return false;
}
else
{
if (src_point.size() > 3) /*大于三个关键点*/
{
for (int j = 1; j < src_point.size(); j++)
{
key_line.push_back(Line(src_point[j - 1], src_point[j])); /*将关键直线压入容器*/
}
key_line.push_back(Line(src_point[0], src_point[src_point.size() - 1]));/*连接最后一个关键点和第一个关键点*/
//内六角边缘和间隔为1的直线交点为关键点
key_point = src_point; /*区边缘点*/
/********************取交点********************************************************/
for (int i = 0; i < src_point.size() - 2; i++) /*拓展关键点*/
{
key_point.push_back(key_line[i] & key_line[i + 2]); /*直线类&操作已重载,返回直线交点*/
}
key_point.push_back(key_line[src_point.size() - 1] & key_line[1]); /*倒数第一条线和第二条线的角点*/
key_point.push_back(key_line[src_point.size() - 2] & key_line[0]); /*倒数第二条线和第一条线的角点*/
}
return true;
}
}
/********************************************************************************/
/* 功能 : 筛选关键点,去除和圆心距离小于半径L_percent%和 */
/* 大于半径H_percent%的点 */
/* 入参 : in_point :关键点集 */
/* cent :中心坐标 */
/* rad :半径 */
/* L_percent :最小百分比 */
/* H_percent :最大百分比 */
/* 出参 : out_point :筛选后的关键点集 */
/* 返回值 : 是否转换成功 */
/********************************************************************************/
void checkPoint(vector<Point>in_point, vector<Point>&out_point, Point cent,
int rad,double L_percent,double H_percent)
{
out_point.clear(); /*清空输出容器*/
for (int i = 0; i < in_point.size(); i++) /*遍历关键点*/
{
int x = in_point[i].x;
int y = in_point[i].y;
int dd = (x - cent.x)*(x - cent.x) + (y - cent.y)*(y - cent.y); /*计算关键点和中心的距离*/
if (dd > (rad*rad*H_percent*H_percent) ||
dd < (rad*rad*L_percent*L_percent))
{
continue;
}
out_point.push_back(in_point[i]); /*合格的关键点装入容器*/
}
}
int main(void)
{
Mat img = imread("5.jpg"); /*输入图片*/
Mat show = img.clone(); /*克隆图像,最后图像显示用*/
/*************霍夫圆检测**********************************************************/
Point cir_cent; /*圆心坐标*/
int rad = 0; /*圆的半径*/
cvtColor(img, img, COLOR_BGR2GRAY); /*图像灰度化*/
vector<Vec3f> cir;
HoughCircles(img, cir, HOUGH_GRADIENT, 2, 10000, 100, 20, 80, 120); /*霍夫圆*/
cir_cent = Point(cir[0][0], cir[0][1]); /*获得圆心坐标*/
rad = cir[0][2]; /*获得圆的半径*/
/*************边缘识别*************************************************************/
vector<vector<Point>> contours; /*边缘点集*/
Mat Contours_img;
threshold(img, Contours_img, 25, 255, THRESH_BINARY_INV); /*图像二值化,使其出现内六边形轮廓*/
imshow("threshold", Contours_img); /*展示二值化后的图像*/
findContours(Contours_img, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE); /*轮廓检测*/
vector<Point>key_point; /*关键点*/
vector<Line>key_line; /*关键直线*/
for (int i = 0; i < contours.size(); i++) /*遍历轮廓*/
{
vector<Point>point; /*储存拟合后的轮廓*/
approxPolyDP(contours[i], point, 6, true); /*拟合边缘*/
if (point.size()<5 || point.size()>15) /*小于5个或大于15个边缘点的边缘舍去*/
{
continue;
}
checkPoint(point, key_point, cir_cent, rad, 1. / 3., 2. / 3.); /*到圆心距离为半径的2/3,舍去*/
getKeyPoint_Line(key_point, key_point, key_line); /*获取关键直线*/
checkPoint(key_point, key_point, cir_cent, rad, 1. / 3., 2. / 3.); /*再次检测*/
/************遍历计算拟合6边形,寻找最优解******************************************/
vector<Hexagon> hexagons;
for (int i = 0; i < key_point.size(); i++)
{
hexagons.push_back(Hexagon(cir_cent,key_point[i]));
circle(img, key_point[i], 10, 100); /*画关键点*/
}
for (int i = 0; i < key_line.size();i++)
{
key_line[i].draw(img, 100); /*画线*/
}
vector<int> err; /*误差容器,储存说有六边形误差*/
for (int i = 0; i < hexagons.size(); i++) /*计算所有关键点误差*/
{
err.push_back(hexagons[i].getError(key_point));
}
int hexagons_index = 0; /*容器中最优六边形下标*/
for (int i = 0; i < err.size(); i++) /*取最小误差*/
{
if (hexagons_index > err[i])
{
hexagons_index = err[i];
}
}
if (hexagons.size()>hexagons_index) /*防止没有关键点容器越界报错*/
{
hexagons[hexagons_index].draw(show, Scalar(0,255,0),1); /*画最优6边形*/
hexagons[hexagons_index].draw(img, 150, 1); /*画最优6边形*/
}
}
imshow("show_", show);
circle(img, cir_cent, rad, 150); /*画霍夫圆*/
imshow("img", img);
waitKey();
return 0;
}
hexagon.h
#ifndef _HEXAGON_H_
#define _HEXAGON_H_
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
/********************************************************************************/
/* 功能 : 一个用于储存正六边形参数的类 */
/********************************************************************************/
class Hexagon
{
public:
Hexagon();
Hexagon(Point2d cent, Point2d point); /*通过中心点和其中一个角点构造一个正六边形*/
Hexagon(Point2d p1, Point2d p2, Point2d p3); /*通过三个连续的点构造一个正六边形*/
void draw(Mat &img, const Scalar& color, /*画六边形*/
int thickness = 1, int LineType = LINE_8, int shift = 0);
int getError(vector<Point> points); /*计算误差六边形*/
~Hexagon();
private:
Point2d nextPoint(Point2d p1, Point2d p2); /*计算正六边形的下一个点坐标*/
public:
Point2d vertex[6]; /*正六边形六个角点*/
};
#endif
hexagon.cpp
#include "Hexagon.h"
Hexagon::Hexagon()
{
}
/********************************************************************************/
/* 功能 : 计算正六边形的下一个点坐标 */
/* 入参 : p0 :角点 */
/* p1 :角点 */
/* 出参 : 下一个角点 */
/********************************************************************************/
Point2d Hexagon::nextPoint(Point2d p0, Point2d p1)
{
double x0 = p0.x;
double y0 = p0.y;
double x1 = p1.x;
double y1 = p1.y;
double x = (x0*x0 - x1*x1 + y0*y0 - y1*y1 - (y0 - y1)*
(y0 + y1 + pow(3., 0.5)*(x0 - x1))) / (2.*(x0 - x1)); /*matlab算出的正六边形递推公司*/
double y = 0.5*(y0 + y1 + (x0-x1)*pow(3., 0.5)); /*matlab算出的正六边形递推公司*/
return Point2d(x,y);
}
/********************************************************************************/
/* 功能 : 通过中心点和其中一个角点构造一个正六边形 */
/* 入参 : cent :中心 */
/* point :角点 */
/* 出参 : 无 */
/********************************************************************************/
Hexagon::Hexagon(Point2d cent, Point2d point)
{
if (cent == point)
{
return;
}
vertex[0] = point;
for (int i = 1; i < 6; i++)
{
vertex[i] = nextPoint(vertex[i - 1], cent); /*构造一个正六边形*/
}
}
/********************************************************************************/
/* 功能 : 通过三个连续的点构造一个正六边形 */
/* 入参 : p1 :角点 */
/* p2 :角点 */
/* p3 :角点 */
/* 出参 : 无 */
/********************************************************************************/
Hexagon::Hexagon(Point2d p1, Point2d p2, Point2d p3)
{
if (p1 == p2 || p1 == p3 || p2 == p3) /*三个点不能相等*/
{
return;
}
vertex[0] = p1;
Point2d cent0 = nextPoint(p1, p2); /*计算中心*/
Point2d cent1 = nextPoint(p2, p3); /*计算中心*/
Point2d cent = (cent0 + cent1) / 2.; /*僵持计算的中心取平均*/
for (int i = 1; i < 6; i++)
{
vertex[i] = nextPoint(vertex[i - 1], cent); /*构造一个正六边形*/
}
}
/********************************************************************************/
/* 功能 : 画六边形 */
/* 入参 : img :图像 */
/* color :颜色 */
/* thickness :线宽 */
/* LineType :线型 */
/* shift :点坐标中的小数位数 */
/* 出参 : 无 */
/* 返回值 : 无 */
/********************************************************************************/
void Hexagon::draw(Mat &img, const Scalar& color,
int thickness, int LineType, int shift)
{
for (int i = 1;i<6;i++)
{
line(img, vertex[i-1], vertex[i], color, thickness, LineType, shift);
}
line(img, vertex[0], vertex[5], color, thickness, LineType, shift);
}
/********************************************************************************/
/* 功能 : 计算误差六边形 */
/* 入参 : points :关键点集 */
/* 出参 : 无 */
/* 返回值 : 误差 */
/********************************************************************************/
int Hexagon::getError(vector<Point> points)
{
vector<int> err(6); /*误差容器*/
for (int i = 0; i < 6; i++) /*遍历六边形6个理论角点*/
{
int err_tmp1 = INT_MAX; /*理论角点和关键点的最小误差*/
int index = -1; /*关键点的下标*/
int x2 = vertex[i].x;
int y2 = vertex[i].y;
for (int j = 0; j < points.size(); j++) /*遍历所有关键点*/
{
int x1 = points[j].x;
int y1 = points[j].y;
int err_tmp2 = (x2 - x1)*(x2 - x1) + (y2 - y1)*(y2 - y1); /*计算关键点和角点的误差*/
if (err_tmp2 < err_tmp1) /*取最小误差作为角点和关键点误差*/
{
err_tmp1 = err_tmp2;
index = j;
}
}
points.erase(points.begin() + index); /*移除该关键点,避免一个关键点匹配多个角点的情况*/
err[i] = err_tmp1; /*获得误差*/
}
int err_all = 0; /*该正六边形的误差*/
sort(err.begin(), err.end()); /*排序所有角点误差*/
if (err.size() > 3) /*必须有4个点*/
{
err_all = abs(err[0]) + abs(err[1]) + abs(err[2]); /*取前3,返回误差为前三误差和*/
return err_all;
}
else
{
return 0;
}
}
Hexagon::~Hexagon()
{
}
line.h
#ifndef _LINE_H_
#define _LINE_H_
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
/********************************************************************************/
/* 功能 : 一个用于储存直线参数的类 */
/********************************************************************************/
class Line
{
public:
Line();
Line(double k,double b); /*通过斜率和截距构造直线类*/
Line(Point2d pt1, Point2d pt2); /*通过两点构造直线类*/
void getFrom2pt(Point2d pt1, Point2d pt2); /*通过两个点来构造直线类*/
void draw(Mat &img, const Scalar& color, /*画直线*/
int thickness = 1, int LineType = LINE_8, int shift = 0);
Point2d Line::operator &(Line L);
friend ostream &operator << (ostream &os,const Line L);
~Line();
public:
double k; /*直线斜率*/
double b; /*直线截距*/
};
#endif
line.cpp
#include "Line.h"
Line::Line()
{
this->b = 0;
this->k = 0;
}
/********************************************************************************/
/* 功能 : 通过斜率和截距构造直线类 */
/* 入参 : k :斜率 */
/* b :截距 */
/* 出参 : 无 */
/********************************************************************************/
Line::Line(double k, double b)
{
this->b = b;
this->k = k;
}
/********************************************************************************/
/* 功能 : 通过两点构造直线类 */
/* 入参 : pt1 :起点 */
/* pt2 :终点 */
/* 出参 : 无 */
/********************************************************************************/
Line::Line(Point2d pt1, Point2d pt2)
{
double y0 = pt1.y, y1 = pt2.y, x0 = pt1.x, x1 = pt2.x;
k = (y0 - y1) / (x0 - x1);
b = y0 - (y0 - y1) / (x0 - x1)*x0;
}
/********************************************************************************/
/* 功能 : 通过两个点来构造直线类 */
/* 入参 : pt1 :起点 */
/* pt2 :终点 */
/* 出参 : 无 */
/* 返回值 : 无 */
/********************************************************************************/
void Line::getFrom2pt(Point2d pt1, Point2d pt2)
{
Line(pt1, pt2);
}
/********************************************************************************/
/* 功能 : 重载与操作符 */
/* 入参 : L :直线 */
/* 出参 : 无 */
/* 返回值 : 交点 */
/********************************************************************************/
Point2d Line::operator &(Line L)
{
Point2d Intersection;
Intersection.x = (L.b - b) / (k - L.k);
Intersection.y = k*Intersection.x + b;
return Intersection;
}
/********************************************************************************/
/* 功能 : 画直线 */
/* 入参 : img :图像 */
/* color :颜色 */
/* thickness :线宽 */
/* LineType :线型 */
/* shift :点坐标中的小数位数 */
/* 出参 : 无 */
/* 返回值 : 无 */
/********************************************************************************/
void Line::draw(Mat &img, const Scalar& color,
int thickness, int LineType, int shift)
{
Point2d draw_p1(10000, k * 10000 + b), draw_p2(-10000, k*(-10000) + b);
line(img, draw_p1, draw_p2, color, thickness, LineType, shift);
}
/********************************************************************************/
/* 功能 : 重载与左移操作符 */
/* 入参 : os :输出流对象 */
/* L :直线 */
/* 出参 : 无 */
/* 返回值 : 输出流对象 */
/********************************************************************************/
ostream &operator << (ostream &os, const Line L)
{
if (L.b>0)
{
os << "y = " << L.k << "x+" << L.b;
}
else
{
os << "y = " << L.k << "x" << L.b;
}
return os;
}
Line::~Line()
{
}