Schwarzer教你用OpenCV实现基于标记的AR
来源:互联网 发布:乐视电视怎么设置网络 编辑:程序博客网 时间:2024/06/04 19:19
导读
本文将一步步教您如何使用OpenCV实现基于一个标记的简单AR
作者开发环境:
Windows 10 (64bit)
Visual Studio 2015
OpenCV 3.2.0
源代码
您可以在此处获取源代码工程 去Github
使用的标记:
您可以打印下来,一张纸上打印多个便识别多个
Step 1 开始
在IDE中创建C++工程,并添加好OpenCV的相关环境配置,添加一个源文件,例如命名为SimpleAR.cpp
添加 include 并 使用命名空间
#include<opencv2\opencv.hpp>#include<iostream>#include<math.h>using namespace cv;using namespace std;
Step 2 类介绍
class MarkerBasedARProcessor{ Mat Image, ImageGray, ImageAdaptiveBinary; //分别是 原图像 灰度图像 自适应阈值化图像 vector<vector<Point>> ImageContours; //图像所有边界信息 vector<vector<Point2f>> ImageQuads, ImageMarkers; //图像所有四边形 与 验证成功的四边形 vector<Point2f> FlatMarkerCorners; //正方形化标记时用到的信息 Size FlatMarkerSize; //正方形化标记时用到的信息 //7x7黑白标记的颜色信息 uchar CorrectMarker[7 * 7] = { 0,0,0,0,0,0,0, 0,0,0,0,0,255,0, 0,0,255,255,255,0,0, 0,255,255,255,0,255,0, 0,255,255,255,0,255,0, 0,255,255,255,0,255,0, 0,0,0,0,0,0,0 }; void Clean(); // 用于新一帧处理前的初始化 void ConvertColor(); //转换图片颜色 void GetContours(int ContourCountThreshold); //获取图片所有边界 void FindQuads(int ContourLengthThreshold); //寻找所有四边形 void TransformVerifyQuads(); //变换为正方形并验证是否为标记 void DrawMarkerBorder(Scalar Color); //绘制标记边界 void DrawImageAboveMarker(); //在标记上绘图 bool MatchQuadWithMarker(Mat & Quad); // 检验正方形是否为标记 float CalculatePerimeter(const vector<Point2f> &Points); // 计算周长public: Mat ImageToDraw;// 要在标记上绘制的图像 MarkerBasedARProcessor();// 构造函数 Mat Process(Mat& Image);// 处理一帧图像};
Step 3 主体流程
首先我们来看main()函数
int main(){ Mat Frame, ProceedFrame; VideoCapture Camera(0); // 初始化相机 while (!Camera.isOpened()); // 等待相机加载完成 MarkerBasedARProcessor Processor; // 构造一个AR处理类 Processor.ImageToDraw = imread("ImageToDraw.jpg"); // 读入绘制图像 while (waitKey(1)) // 每次循环延迟1ms { Camera >> Frame; // 读一帧 imshow("Frame", Frame); // 显示原始图像 ProceedFrame = Processor.Process(Frame); // 处理图像 imshow("ProceedFrame", ProceedFrame); // 显示结果图像 }}
很显然,接下来进一步查看Process函数中发生了什么
Mat Process(Mat& Image) { Clean(); // 新一帧初始化 Image.copyTo(this->Image); // 复制原始图像到Image中 ConvertColor(); // 转换颜色 GetContours(50); // 获取边界 FindQuads(100); // 寻找四边形 TransformVerifyQuads(); // 变形并校验四边形 DrawMarkerBorder(Scalar(255, 255, 255)); // 在得到的标记周围画边界 DrawImageAboveMarker(); // 在标记上画图 return this->Image; // 返回结果图案 }
一个最简单的AR就完成了。
让我们列出要经历的步骤:
1. 转换图像颜色(cvtColor,adaptiveThreshold)
2. 拿自适应阈值化(adaptiveThreshold)
后图像获取(findContours)
图形中所有边界
3. 寻找(approxPolyDP)
所有边界中的四边形
4. 把图像中扭曲的四边形转换(getPerspectiveTransform,warpPerspective)
为正方形
5. 用二值化后的图像与正确标记的颜色对比
6. 得到的标记坐标拿来绘制图像
7. 享受胜利的果实接下来就开始分部说明
Step 4 转换颜色
最简单的步骤
首先初始化
void Clean() { ImageContours.clear(); ImageQuads.clear(); ImageMarkers.clear(); }
然后转换颜色
void ConvertColor() { cvtColor(Image, ImageGray, CV_BGR2GRAY); adaptiveThreshold(ImageGray, ImageAdaptiveBinary, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY_INV, 7, 7); }
分别把灰度图像和自适应阈值化图像保存至
ImageGray
和ImageAdaptiveBinary
Step 5 获取边界
参数说明:
int ContourCountThreshold
最大边界数量阈值
四边形有可能不是有4个顶点构成的,稍后需要拟合,此值设置为50
void GetContours(int ContourCountThreshold) { vector<vector<Point>> AllContours; // 所有边界信息 findContours(ImageAdaptiveBinary, AllContours, CV_RETR_LIST, CV_CHAIN_APPROX_NONE); // 用自适应阈值化图像寻找边界 for (size_t i = 0;i < AllContours.size();++i) // 只储存低于阈值的边界 { int contourSize = AllContours[i].size(); if (contourSize > ContourCountThreshold) { ImageContours.push_back(AllContours[i]); } } }
结束后
ImageContour
储存了需要的边界信息
Step 6 寻找四边形
参数说明:
int ContourLengthThreshold
最小四边形边长阈值
void FindQuads(int ContourLengthThreshold) { vector<vector<Point2f>> PossibleQuads; for (int i = 0;i < ImageContours.size();++i) { vector<Point2f> InDetectPoly; approxPolyDP(ImageContours[i], InDetectPoly, ImageContours[i].size() * 0.05, true); // 对边界进行多边形拟合 if (InDetectPoly.size() != 4) continue;// 只对四边形感兴趣 if (!isContourConvex(InDetectPoly)) continue; // 只对凸四边形感兴趣 float MinDistance = 1e10; // 寻找最短边 for (int j = 0;j < 4;++j) { Point2f Side = InDetectPoly[j] - InDetectPoly[(j + 1) % 4]; float SquaredSideLength = Side.dot(Side); MinDistance = min(MinDistance, SquaredSideLength); } if (MinDistance < ContourLengthThreshold) continue; // 最短边必须大于阈值 vector<Point2f> TargetPoints; for (int j = 0;j < 4;++j) // 储存四个点 { TargetPoints.push_back(Point2f(InDetectPoly[j].x, InDetectPoly[j].y)); } Point2f Vector1 = TargetPoints[1] - TargetPoints[0]; // 获取一个边的向量 Point2f Vector2 = TargetPoints[2] - TargetPoints[0]; // 获取一个斜边的向量 if (Vector2.cross(Vector1) < 0.0) // 计算两向量的叉乘 判断点是否为逆时针储存 swap(TargetPoints[1], TargetPoints[3]); // 如果大于0则为顺时针,需要交替 PossibleQuads.push_back(TargetPoints); // 保存进可能的四边形,进行进一步判断 }
至此获得了一些被逆时针储存的,可能为标记的四边形坐标
vector<pair<int, int>> TooNearQuads; // 准备删除几组靠太近的多边形 for (int i = 0;i < PossibleQuads.size();++i) { vector<Point2f>& Quad1 = PossibleQuads[i]; // 第一个 for (int j = i + 1;j < PossibleQuads.size();++j) { vector<Point2f>& Quad2 = PossibleQuads[j]; // 第二个 float distSquared = 0; float x1Sum = 0.0, x2Sum = 0.0, y1Sum = 0.0, y2Sum = 0.0, dx = 0.0, dy = 0.0; for (int c = 0;c < 4;++c) { x1Sum += Quad1[c].x; x2Sum += Quad2[c].x; y1Sum += Quad1[c].y; y2Sum += Quad2[c].y; } x1Sum /= 4; x2Sum /= 4; y1Sum /= 4; y2Sum /= 4; // 计算平均值(中点) dx = x1Sum - x2Sum; dy = y1Sum - y2Sum; distSquared = sqrt(dx*dx + dy*dy); // 计算两多边形距离 if (distSquared < 50) { TooNearQuads.push_back(pair<int, int>(i, j)); // 过近则准备剔除 } } }
至此我们一一比较了多边形们,将距离过近的挑选了出来
vector<bool> RemovalMask(PossibleQuads.size(), false); // 移除标记列表 for (int i = 0;i < TooNearQuads.size();++i) { float p1 = CalculatePerimeter(PossibleQuads[TooNearQuads[i].first]); //求周长 float p2 = CalculatePerimeter(PossibleQuads[TooNearQuads[i].second]); int removalIndex; //移除周长小的多边形 if (p1 > p2) removalIndex = TooNearQuads[i].second; else removalIndex = TooNearQuads[i].first; RemovalMask[removalIndex] = true; }
至此我们标记出周长小的相邻多边形,并在下一步储存中跳过他
for (size_t i = 0;i < PossibleQuads.size();++i) { // 只录入没被剔除的多边形 if (!RemovalMask[i]) ImageQuads.push_back(PossibleQuads[i]); } }
计算边长函数如下
float CalculatePerimeter(const vector<Point2f> &Points) //求多边形周长 { float sum = 0, dx, dy; for (size_t i = 0;i < Points.size();++i) { size_t i2 = (i + 1) % Points.size(); dx = Points[i].x - Points[i2].x; dy = Points[i].y - Points[i2].y; sum += sqrt(dx*dx + dy*dy); } return sum; }
Step 7 变形与校验
我们需要把扭曲的标记转换为正方形来进行判断,使用getPerspectiveTransform函数可以帮助我们实现这一点,他接受2个参数,分别是源图像中的四点坐标与正方形图像中的四点坐标。
源图像的四点坐标即上面我们得到的
ImageQuads
。正方形图像的四点坐标即一开始在类介绍环节您可能产生疑问的
FlatMarkerCorners
,因为我们把他存入新的图像中,实际上就是新图像的四个顶点。它返回一个变换矩阵,我们将他交给下一步
warpPerspective
中,即可从原图像中获取裁剪下来的变为正方形的可能标记了。于此同时,类介绍中的FlatMarkerSize
在这里也起了作用,他是用来告诉函数生成图像的大小的。这两个变量在类的构造函数中定义:
MarkerBasedARProcessor() { FlatMarkerSize = Size(35, 35); FlatMarkerCorners = { Point2f(0,0),Point2f(FlatMarkerSize.width - 1,0), Point2f(FlatMarkerSize.width - 1,FlatMarkerSize.height - 1), Point2f(0,FlatMarkerSize.height - 1) }; }
可见正方形四点坐标是由大小决定的。
下面正式进入函数
void TransformVerifyQuads() { Mat FlatQuad; for (size_t i = 0;i < ImageQuads.size();++i) { vector<Point2f>& Quad = ImageQuads[i]; Mat TransformMartix = getPerspectiveTransform(Quad, FlatMarkerCorners); warpPerspective(ImageGray, FlatQuad, TransformMartix, FlatMarkerSize);
正方形图像已经存入
FlatQuad
中
threshold(FlatQuad, FlatQuad, 0, 255, THRESH_OTSU); // 变为二值化图像 if (MatchQuadWithMarker(FlatQuad)) // 与正确标记比对 { ImageMarkers.push_back(ImageQuads[i]); // 成功则记录 } else // 如果失败,则旋转,每次90度进行比对 { for (int j = 0;j < 3;++j) { rotate(FlatQuad, FlatQuad, ROTATE_90_CLOCKWISE); if (MatchQuadWithMarker(FlatQuad)) { ImageMarkers.push_back(ImageQuads[i]); // 成功则记录 break; } } } } }
比对函数如下
bool MatchQuadWithMarker(Mat & Quad) { int Pos = 0; for (int r = 2;r < 33;r += 5) // 正方形图像大小为(35,35) { for (int c = 2;c < 33;c += 5)// 读取每块图像中心点 { uchar V = Quad.at<uchar>(r, c); uchar K = CorrectMarker[Pos]; if (K != V) // 与正确标记颜色信息比对 return false; Pos++; } } return true; }
Step 8 绘图
接下来到了最后一步
首先绘制边界
void DrawMarkerBorder(Scalar Color) { for (vector<Point2f> Marker : ImageMarkers) { line(Image, Marker[0], Marker[1], Color, 2, CV_AA); line(Image, Marker[1], Marker[2], Color, 2, CV_AA); line(Image, Marker[2], Marker[3], Color, 2, CV_AA); line(Image, Marker[3], Marker[0], Color, 2, CV_AA);//CV_AA是抗锯齿 } }
最后将图像绘制到标记上,方法类似于变为正方形,只不过是由标准矩形图像变为扭曲的标记坐标而已。
void DrawImageAboveMarker() { if (ImageToDraw.empty())return; vector<Point2f> ImageCorners = { Point2f(0,0),Point2f(ImageToDraw.cols - 1,0), Point2f(ImageToDraw.cols - 1,ImageToDraw.rows - 1), Point2f(0,ImageToDraw.rows - 1) }; // 与变为正方形类似,也需要这样的四个顶点 Mat_<Vec3b> ImageWarp = Image; // 便于操作像素点 for (vector<Point2f> Marker : ImageMarkers) { Mat TransformMartix = getPerspectiveTransform(ImageCorners, Marker); Mat_<Vec3b> Result(Size(Image.cols, Image.rows), CV_8UC3); warpPerspective(ImageToDraw, Result, TransformMartix, Size(Image.cols, Image.rows));
先求出旋转矩阵,然后得到变换后的图像,并不是直接绘制到原图像上的,得到的图像除了标记的区域其他全为黑色
把变换后的图像非黑色的部分绘制到原图像上
for (int r = 0;r < Image.rows;++r) { for (int c = 0;c < Image.cols;++c) { if (Result(r, c) != Vec3b(0, 0, 0)) { ImageWarp(r, c) = Result(r, c); } } } } }
Step 9 编译,运行,享受胜利的果实
Step Extra 不足
标记有一点不全或遮挡都会失败
没有统一标记方向的储存
所以才是最简单的AR
附上我学习的博文链接:
http://blog.csdn.net/chuhang_zhqr/article/details/50034669
http://blog.csdn.net/chuhang_zhqr/article/details/50036443
- Schwarzer教你用OpenCV实现基于标记的AR
- 基于标记的AR的OpenCV实现
- 基于标记的AR的OpenCV实现
- 基于标记的AR的opencv实现(一)
- 基于标记的AR的OpenCV实现(二)
- 基于标记的AR的OpenCV实现:动态视频输入
- 基于标记的AR的opencv实现(一)
- 基于标记的AR的OpenCV实现(二)
- 基于标记的AR的opencv实现(一)
- 基于标识的AR的OpenCV实现(三)
- 基于OpenCV实现的Android移动端口红AR
- 连通区域标记:c++版的bwlabel实现(基于opencv)
- 基于标记的ID检测opencv实现代码
- 如何使用OpenCV实现基于标记的定位(为什么对标记检测能够实现定位)
- 《Mastering opencv....读书笔记》基于标记的虚拟现实
- 简单的 AR 效果实现【OpenGL】【OpenCV】
- ios--OpenCV--基于模板图片的标记识别
- iOS开发005 OpenCV--基于模板图片的标记识别
- 自动生成unpivot,方便table展示字符度量
- OpenCL快速入门教程
- android左右滑动监听,上下滑动
- shiro入门
- Android4.4系统浏览器Chromium实现的加载模块与流程
- Schwarzer教你用OpenCV实现基于标记的AR
- TCP漏洞引来黑客对Linux系统的攻击
- android的Activity采用透明主题
- 用python连接数据库,实现用户注册和用户登录
- java-字符串把所有的a改为b
- 行业 | 角逐无人驾驶:美加州无人车测试公司已达30多家
- 进程同步--生产者消费者问题(Producer-consumer Problem)
- bzoj1143-最小链覆盖&偏序集定理&二分图-祭祀river
- 回顾大专三年的学习