最近学习AR,买了本Mastering OpenCV,这书上有两个AR的例子,这里先分析的是第二章基于标识的AR,书中是使用Xcode给iphone或者ipad写的,本文是在linux系统上vim实现的,终端模式。
先推荐两个前辈的博客,本文参考了二者和书进行理解源码。
http://blog.csdn.net/jinshengtao/article/details/48604435 taotao1233写的,大部分是基于他的博客,称转载也可以。
http://blog.csdn.net/acorld/article/details/8747813 missjuan写的,就是分析在xcode上进行实现的。
我的程序源码已经上传到http://download.csdn.net/detail/chuhang_zhqr/9298975,有需要的请下载。
以下开始进行分析:
程序大体框架:
1:对输入图像帧进行标记检测,这里包括,灰度化,找到图像中轮廓,搜索可能的标记;检测和解码标记,
2:估计标记的三维姿态,这里包括提前对摄像机进行相机标定,获取相机内参数和失真系数,根据这个计算出标记的旋转矩阵和平移矩阵。
3:由相机内参数和标记的旋转矩阵和平移矩阵,用OpenGL进行渲染三维物体;
以上是实现AR的必经之路,我认为OpenCV实现就已经很底层了,再底层那就太麻烦了,在科研的道路上,工程化实现未尝不可。
因为是刚把源码实现了,没来及完善工程,决定趁热打铁先记录一下心路历程。
现在开始分析源码,从第一步开始,输入图像:输入图像帧无非三种源,图片,视频,摄像头。和书上一样先使用图片吧。就是这个图片
书上的不是640x480的,我修了一下。src = imread("1.jpg");
1):灰度化处理:
必须将输入图像转换为灰度图,因为标识仅包含黑白块,这使得更容易在灰度图像上操作这些块。
//彩色转换成灰色图像
cvtColor(src,grayscale,CV_BGRA2GRAY);
}
这个没什么好说的,
2):执行二值化阈值操作:
将图像的每个像素变成黑色或白色,为检测轮廓做准备,使用合理的自适应阈值法,减小光照条件和软强度变化影响。以需要二值化的像素为中心,将给定半径内的所有像素的平均强度作为该像素的强度,使轮廓检测更具有鲁棒性。
adaptiveThreshold(grayscale,//Input Image
thresholdImg,//Result binary image
255,
ADAPTIVE_THRESH_GAUSSIAN_C,
THRESH_BINARY_INV,
7,
7
);
/*输入图像
//输出图像
//使用 CV_THRESH_BINARY 和 CV_THRESH_BINARY_INV 的最大值
//自适应阈值算法使用:CV_ADAPTIVE_THRESH_MEAN_C 或 CV_ADAPTIVE_THRESH_GAUSSIAN_C
//取阈值类型:必须是下者之一
//CV_THRESH_BINARY,
//CV_THRESH_BINARY_INV
//用来计算阈值的象素邻域大小: 3, 5, 7, ...
*/
3):轮廓检测:
使用findContours检测输入图像的轮廓。
//检测所输入的二值图像的轮廓,返回一个多边形列表,其每个多边形标识一个轮廓,小轮廓不关注,不包括标记...
void MarkerDetector::findContour(cv::Mat& thresholdImg, ContoursVector& contours, int minContourPointsAllowed) const
{
ContoursVector allContours;
/*输入图像image必须为一个2值单通道图像
//检测的轮廓数组,每一个轮廓用一个point类型的vector表示
//轮廓的检索模式
CV_RETR_EXTERNAL表示只检测外轮廓
CV_RETR_LIST检测的轮廓不建立等级关系
CV_RETR_CCOMP建立两个等级的轮廓,上面的一层为外边界,里面的一层为内孔的边界信息。如果内孔内还有一个连通物体,这个物体的边界也在顶层。
CV_RETR_TREE建立一个等级树结构的轮廓。具体参考contours.c这个demo
//轮廓的近似办法
CV_CHAIN_APPROX_NONE存储所有的轮廓点,相邻的两个点的像素位置差不超过1,即max(abs(x1-x2),abs(y2-y1))==1
CV_CHAIN_APPROX_SIMPLE压缩水平方向,垂直方向,对角线方向的元素,只保留该方向的终点坐标,例如一个矩形轮廓只需4个点来保存轮廓信息
CV_CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS使用teh-Chinl chain 近似算法
offset表示代表轮廓点的偏移量,可以设置为任意值。对ROI图像中找出的轮廓,并要在整个图像中进行分析时,这个参数还是很有用的。
*/
findContours(thresholdImg, allContours, CV_RETR_LIST, CV_CHAIN_APPROX_NONE);
contours.clear();
for(size_t i=0;i<allContours.size();i++)
{
int contourSize = allContours[i].size();
if(contourSize > minContourPointsAllowed)
{
contours.push_back(allContours[i]);
}
}
//Mat result(src.size(),CV_8U,Scalar(0));
//drawContours(result,detectedMarkers,-1,Scalar(255),2);
//imshow("AR based marker...",result);
}
其函数返回值为一个多边形列表,其每个多边形都表示一个轮廓。若轮廓包含的像素数比minContourPointsAllowed还小,则是一个小轮廓,这里不感兴趣,直接去除,这些小轮廓可能并没有包含标记。
4):搜索候选标记:
在找到轮廓后,将开始进行多边形逼近,这样做为了减少轮廓的像素。因为标记总是被四个顶点的多边形包含,如果不是四个,就不是我们想要的标记。筛选出非标记区域。
用Opencv内置API检测多边形,通过判断多边形定点数量是否为4,四边形各顶点之间相互距离是否满足要求(四边形是否足够大),过滤非候选区域。然后再根据候选区域之间距离进一步筛选,得到最终的候选区域,并使得候选区域的顶点坐标逆时针排列。
a. 四边形顶点之间距离
对每个四边形S,计算其相邻顶点之间的距离:
上式中i,j为相邻的两个顶点,若顶点之间的最小值仍大于阈值,则保留该四边形S进行下一步判断。
b. 四边形之间距离
求四边形S和S’之间的距离,即计算四个顶点之间的平均距离:
若dist小于阈值,则四边形S和S’距离较近,记录到tooNearCandidates向量里。接来下perimeter函数分别求四边形S和S’四个顶点之间的距离和,保留距离较大的,将距离较小的放入removalMask数组中,下式中i,j为四边形内相邻顶点
c. 行列式的几何意义—逆时针排序
行列式是由一些数据排列成的方阵经过规定的计算方法而得到的一个数。那它的几何意义是什么呢?有两种解释:
一个是行列式就是行列式中的行或列向量所构成的超平行多面体的有向面积或有向体积;
另一个是矩阵A的行列式detA就是线性变化A下的图形面积或体积的伸缩因子;
接下来的代码中,由于在approxPolyDP寻找多边形时,顶点摆放次序有逆时针和顺时针两种,我们希望这些顶点按照逆时针摆放。因此,对于四边形而言,我们只讨论2*2行列式对应的有向面积。
一个2×2矩阵A的行列式,是A的行向量(或列向量)决定的平行四边形的有向面积。用几何观点来看,二阶行列式D是XOY平面上以行向量a=(a1,a2),b=(b1,b2)为邻边的平行
四边形的有向面积。若这个平行四边形是由向量a沿逆时针方向转到b而得到的,面积取正值;若这个平行四边形是由向量a沿顺时针方向转到而得到的,面积取负值。本例中,对于顺时针摆放的顶点0,1,2,3,咱可以通过计算有0-1,0-2构成的向量计算其有向面积。如果是顺时针摆放,那么该有向面积一定是负数,只要交换1,3位置即可。
//由于我们的标记是四边形,当找到图像所有轮廓细节后,本文用Opencv内置API检测多边形,通过判断多边形定点数量是否为4,四边形各顶点之间相互距离是否满足要求(四边形是否足够大),过滤非候选区域。然后再根据候选区域之间距离进一步筛选,得到最终的候选区域,并使得候选区域的顶点坐标逆时针排列。
void MarkerDetector::findCandidates(const ContoursVector& contours,vector<Marker>& detectedMarkers)
{
vector<Point> approxCurve;//返回结果为多边形,用点集表示//相似形状
vector<Marker> possibleMarkers;//可能的标记