视觉的方法可以用来估计位置和姿态。最容易想到的是在目标上布置多个容易识别的特征,这样使用opencv相机标定和、相机畸变矫正、轮廓提取、solvepnp来获取目标相对于相机的位姿。在实际使用中只要相机和目标一方是估计的,那就可以得到全局坐标。如果相机和靶标都在移动,那只能获取到相对坐标。但是受限于相机视角和景深,这样多个特征的识别虽然精度可以很高,但是范围却很小。
对于如何扩大范围,使用二维码是一个很好的思路。首先,二维码本身具有多个特征,单个二维码可以用来实现上述方法的功能。其次,二维码本身带有信息,如果二维码的布置事先已知,那么位置和姿态估计的范围将只受限于二维码的数量。
本文主要是二维码的特征识别和信息识别。
二维码是在一个网站上生成的,经过手机的测试,生成的二维码没有问题。生成的图片及二维码各种参数可以自定义。
本文的测试图片有20张,是数字1到5,每个数字隔90度旋转各一张。
二维码特征识别的思路是:第一步,寻找二维码的三个角的定位角点,需要对图片进行平滑滤波,二值化,寻找轮廓,筛选轮廓中有两个子轮廓的特征,从筛选后的轮廓中找到面积最接近的3个即是二维码的定位角点。第二步:判断3个角点处于什么位置,主要用来对图片进行透视校正(相机拍到的图片)或者仿射校正(对网站上生成的图片进行缩放拉伸旋转等操作后得到的图片)。需要判断三个角点围成的三角形的最大的角就是二维码左上角的点。然后根据这个角的两个边的角度差确定另外两个角点的左下和右上位置。第三步,根据这些特征识别二维码的范围。
具体的代码:
Mat src = imread( "pic\\456.jpg", 1 );
if(src.empty())
{
fprintf(stderr, "Can not load image!\n");
return 0;
}
Mat src_all=src.clone();
//彩色图转灰度图
Mat src_gray;
cvtColor( src, src_gray, CV_BGR2GRAY );
//对图像进行平滑处理
blur( src_gray, src_gray, Size(3,3) );
//使灰度图象直方图均衡化
equalizeHist( src_gray, src_gray );
namedWindow("src_gray");
imshow("src_gray",src_gray); //灰度图
//指定112阀值进行二值化
Mat threshold_output;
threshold( src_gray, threshold_output, 112, 255, THRESH_BINARY );
#ifdef DEBUG
namedWindow("二值化后输出");
imshow("二值化后输出",threshold_output); //二值化后输出
#endif
//需要的变量定义
Scalar color = Scalar(1,1,255 );
vector<vector<Point>> contours,contours2;
vector<Vec4i> hierarchy;
Mat drawing = Mat::zeros( src.size(), CV_8UC3 );
//Mat drawing2 = Mat::zeros( src.size(), CV_8UC3 );
Mat drawingAllContours = Mat::zeros( src.size(), CV_8UC3 );
//利用二值化输出寻找轮廓
findContours(threshold_output, contours, hierarchy, CV_RETR_TREE, CHAIN_APPROX_NONE, Point(0, 0) );
//寻找轮廓的方法
int tempindex1 = 0;
int tempindex2 = 0;
for(int i = 0;i<contours.size();i++)
{
if(hierarchy[i][2] == -1)
continue;
else
tempindex1 = hierarchy[i][2]; //第一个子轮廓的索引
if(hierarchy[tempindex1][2] == -1)
continue;
else
{
tempindex2 = hierarchy[tempindex1][2]; //第二个子轮廓的索引
//记录搜索到的有两个子轮廓的轮廓并把他们的编号存储
in.a1 = i;
in.a2 = tempindex1;
in.a3 = tempindex2;
vin.push_back(in);
}
}
//按面积比例搜索
vector<index>::iterator it;
for(it = vin.begin();it != vin.end();)
{
vector<Point> out1Contours = contours[it->a1];
vector<Point> out2Contours = contours[it->a2];
double lenth1 = arcLength(out1Contours,1);
double lenth2 = arcLength(out2Contours,1);
if(abs(lenth1/lenth2-2)>1)
{
it = vin.erase(it);
}
else
{
drawContours( drawing, contours, it->a1, CV_RGB(255,255,255) , CV_FILLED, 8);
it++;
}
}
//获取三个定位角的中心坐标
Point point[3];
int i = 0;
vector<Point> pointthree;
for(it = vin.begin(),i = 0;it != vin.end();i++,it++)
{
point[i] = Center_cal( contours, it->a1 );
pointthree.push_back(point[i]);
}
if(pointthree.size() <3)
{
cout << "找到的定位角点不足3个"<<endl;
return 0;
}
//计算轮廓的面积,计算定位角的面积,从而计算出边长
double area = contourArea(contours[vin[0].a1]);
int area_side = cvRound (sqrt (double(area)));
for(int i=0; i<3; i++)
{
//画出三个定位角的中心连线
line(drawing,point[i%3],point[(i+1)%3],color,area_side/10,8);
}
//清除找到的3个点,以便处理下一幅图片使用
vin.clear();
//由3个定位角校正图片
//=========================================
//找到角度最大的点
double ca[2];
double cb[2];
ca[0] = pointthree[1].x - pointthree[0].x;
ca[1] = pointthree[1].y - pointthree[0].y;
cb[0] = pointthree[2].x - pointthree[0].x;
cb[1] = pointthree[2].y - pointthree[0].y;
double angle1 = 180/3.1415*acos((ca[0]*cb[0]+ca[1]*cb[1])/(sqrt(ca[0]*ca[0]+ca[1]*ca[1])*sqrt(cb[0]*cb[0]+cb[1]*cb[1])));
double ccw1;
if(ca[0]*cb[1] - ca[1]*cb[0] > 0) ccw1 = 0;
else ccw1 = 1;
ca[0] = pointthree[0].x - pointthree[1].x;
ca[1] = pointthree[0].y - pointthree[1].y;
cb[0] = pointthree[2].x - pointthree[1].x;
cb[1] = pointthree[2].y - pointthree[1].y;
double angle2 = 180/3.1415*acos((ca[0]*cb[0]+ca[1]*cb[1])/(sqrt(ca[0]*ca[0]+ca[1]*ca[1])*sqrt(cb[0]*cb[0]+cb[1]*cb[1])));
double ccw2;
if(ca[0]*cb[1] - ca[1]*cb[0] > 0) ccw2 = 0;
else ccw2 = 1;
ca[0] = pointthree[1].x - pointthree[2].x;
ca[1] = pointthree[1].y - pointthree[2].y;
cb[0] = pointthree[0].x - pointthree[2].x;
cb[1] = pointthree[0].y - pointthree[2].y;
double angle3 = 180/3.1415*acos((ca[0]*cb[0]+ca[1]*cb[1])/(sqrt(ca[0]*ca[0]+ca[1]*ca[1])*sqrt(cb[0]*cb[0]+cb[1]*cb[1])));
double ccw3;
if(ca[0]*cb[1] - ca[1]*cb[0] > 0) ccw3 = 0;
else ccw3 = 1;
CvPoint2D32f poly[4];
if(angle3>angle2 && angle3>angle1)
{
if(ccw3)
{
poly[1] = pointthree[1];
poly[3] = pointthree[0];
}
else
{
poly[1] = pointthree[0];
poly[3] = pointthree[1];
}
poly[0] = pointthree[2];
Point temp(pointthree[0].x + pointthree[1].x - pointthree[2].x , pointthree[0].y + pointthree[1].y - pointthree[2].y );
poly[2] = temp;
}
else if(angle2>angle1 && angle2>angle3)
{
if(ccw2)
{
poly[1] = pointthree[0];
poly[3] = pointthree[2];
}
else
{
poly[1] = pointthree[2];
poly[3] = pointthree[0];
}
poly[0] = pointthree[1];
Point temp(pointthree[0].x + pointthree[2].x - pointthree[1].x , pointthree[0].y + pointthree[2].y - pointthree[1].y );
poly[2] = temp;
}
else if(angle1>angle2 && angle1 > angle3)
{
if(ccw1)
{
poly[1] = pointthree[1];
poly[3] = pointthree[2];
}
else
{
poly[1] = pointthree[2];
poly[3] = pointthree[1];
}
poly[0] = pointthree[0];
Point temp(pointthree[1].x + pointthree[2].x - pointthree[0].x , pointthree[1].y + pointthree[2].y - pointthree[0].y );
poly[2] = temp;
}
CvPoint2D32f trans[4];
int temp = 50;
trans[0] = Point2f(0+temp,0+temp);
trans[1] = Point2f(0+temp,100+temp);
trans[2] = Point2f(100+temp,100+temp);
trans[3] = Point2f(100+temp,0+temp);
//获取透视投影变换矩阵
CvMat *warp_mat = cvCreateMat(3, 3, CV_32FC1);
cvGetPerspectiveTransform(poly, trans, warp_mat);
//计算变换结果
IplImage ipl_img(src_all);
IplImage *dst = cvCreateImage(cvSize(1000, 1000), 8, 3);
cvWarpPerspective(&ipl_img,dst,warp_mat);
//=========================================
#ifdef DEBUG
namedWindow("透视变换后的图");
cvShowImage("透视变换后的图",dst); //透视变换后的图
drawContours( drawingAllContours, contours, -1, CV_RGB(255,255,255) , 1, 8);
namedWindow("DrawingAllContours");
imshow( "DrawingAllContours", drawingAllContours );
namedWindow(pathtemp);
imshow(pathtemp , drawing ); //3个角点填充
#endif
//接下来要框出这整个二维码
Mat gray_all,threshold_output_all;
vector<vector<Point> > contours_all;
vector<Vec4i> hierarchy_all;
cvtColor( drawing, gray_all, CV_BGR2GRAY );
threshold( gray_all, threshold_output_all, 45, 255, THRESH_BINARY );
findContours( threshold_output_all, contours_all, hierarchy_all, RETR_EXTERNAL, CHAIN_APPROX_NONE, Point(0, 0) );//RETR_EXTERNAL表示只寻找最外层轮廓
Point2f fourPoint2f[4];
//求最小包围矩形
RotatedRect rectPoint = minAreaRect(contours_all[0]);
//将rectPoint变量中存储的坐标值放到 fourPoint的数组中
rectPoint.points(fourPoint2f);
for (int i = 0; i < 4; i++)
{
line(src_all, fourPoint2f[i%4], fourPoint2f[(i + 1)%4],
Scalar(20,21,237), 3);
}
namedWindow(pathtemp);
imshow(pathtemp , src_all );
//截取二维码区域
CvSize size= cvSize(200,200);//区域大小
cvSetImageROI(dst,cvRect(0,0,size.width, size.height));//设置源图像ROI
IplImage* pDest = cvCreateImage(size,dst->depth,dst->nChannels);//创建目标图像
cvCopy(dst,pDest); //复制图像
cvSaveImage("Roi.jpg",pDest);//保存目标图像
二维码的信息识别使用的是zbar,一个开源的二维码识别库,经过测试,对图像进行平滑,灰度等处理后识别效率还是很高的。zbar的算法流程简介:https://www.cdsy.xyz/computer/programme/algorithm/230327/cd42021.html。
//对截取后的区域进行解码
Mat imageSource = cv::Mat(pDest);
cvResetImageROI(pDest);//源图像用完后,清空ROI
cvtColor( imageSource, imageSource, CV_BGR2GRAY ); //zbar需要输入灰度图像才能很好的识别
//Zbar二维码识别
ImageScanner scanner;
scanner.set_config(ZBAR_NONE, ZBAR_CFG_ENABLE, 1);
int width1 = imageSource.cols;
int height1 = imageSource.rows;
uchar *raw = (uchar *)imageSource.data;
Image imageZbar(width1, height1, "Y800", raw, width1 * height1);
scanner.scan(imageZbar); //扫描条码
Image::SymbolIterator symbol = imageZbar.symbol_begin();
if(imageZbar.symbol_begin()==imageZbar.symbol_end())
{
cout<<"查询条码失败,请检查图片!"<<endl;
}
for(;symbol != imageZbar.symbol_end();++symbol)
{
cout<<"类型:"<<endl<<symbol->get_type_name()<<endl;
cout<<"条码:"<<endl<<symbol->get_data()<<endl;
}
imageZbar.set_data(NULL,0);
运行结果1:网站上生成的二维码,依次是原图,二值化,角点定位,旋转矫正,识别结果。
运行结果2:相机拍摄的名片上二维码,依次是灰度图,二值化,角点定位,透视矫正,识别结果。