本帖最后由 mumumusuc 于 2016-7-12 23:24 编辑
上篇:Opencv4Android分享:OpenCV与JNI/NDK
本片内容主要是寻找和匹配一种已知标记,完成对对象的分类、跟踪和信息处理等。
你可以在《深入理解OpenCV 实用计算机视觉项目解析》这本书里找到IOS上的相关代码,这篇文章也只是把这本书上的代码移植到Android平台。
一 .效果图
二 .C++实现
先在VS2015+OpenCV2.4.13上验证算法。
直接借用书上5x5的标记,其对应的信息为
- uchar _code[] = { 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1 };
复制代码 简单的来说,如果检测到一个矩形标记并且携带信息与给定标记相同,则能标记处这个物体的位置、景深、方向和携带数据等信息,如第二张图就在检测到的标记上绘制了一个立方体。先从静态图片开始:
1.提取标记轮廓
使用矩形标记的很大原因就是方便检测,并且不会受到旋转等因素的干扰。提取轮廓的过程为常用的一套:
灰度化--滤波--阈值化--提取轮廓--排除无用的轮廓--提取轮廓矩形的4个定点坐标
- //灰度化
- cvtColor(origin, _src, CV_RGB2GRAY);
- //滤波
- //GaussianBlur(_src, _src, Size(5, 5), 0);
- //阈值化
- //threshold(_src, _src, 125, 255, CV_THRESH_BINARY | CV_THRESH_OTSU);
- adaptiveThreshold(_src, _src, 255, CV_ADAPTIVE_THRESH_GAUSSIAN_C, CV_THRESH_BINARY, 13, 5);
- //轮廓
- vector<vector<Point> > contours;
- vector<Vec4i> hierarchy;
- findContours(_src, contours, hierarchy, CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE, Point(0, 0));
- //检测
- vector<vector<Point> > polys;
- vector<Point> poly;
- float minLength = 100*ratio;
- for (int i = 0; i < hierarchy.size(); i++){
- approxPolyDP(contours[i], poly, 5.f, true); //对提取的轮廓进行多边形逼近筛选
- if (poly.size() != 4) continue; //只要4个顶点的轮廓
- if (!isContourConvex(poly)) continue; //只要凸多边形
- if (getDistance(poly[1], poly[0]) < minLength) continue; //逼近多边形边长要大于最小值
- if (getDistance(poly[3], poly[0]) < minLength) continue;
- polys.push_back(poly); //把符合要求的轮廓顶点放到序列当中
- }
复制代码根据使用环境调整参数取值,比如图像大小、环境光线等等。
注意到标记应该是检查目标的最小单元:即目标标记应该不再包含其他标记。上面我只把小于最小边长的轮廓筛选出来了,对于更大的轮廓却没有进行处理。
<-筛选前,筛选后->
2.处理标记轮廓
假设你已经获取了一系列待处理的矩形轮廓的顶点坐标,这些坐标的起点、顺序都是不同的。为了接下来的比对查找,应当使这些顶点顺序排列。这里我希望它们逆时针排列:
- Point t;
- for(int i=0;i<polys.size();i++)
- if(!isAntiClockWise(polys[i][0],polys[i][1],polys[i][3])){
- t = polys[i][1];
- polys[i][1] = polys[i][3];
- polys[i][3] = t;
- }
复制代码- bool isAntiClockWise(Point o,Point a,Point b){
- Point oa = Point(a.x - o.x,a.y - o.y);
- Point ob = Point(b.x - o.x,b.y - o.y);
- if(oa.x*ob.y - oa.y*ob.x < 0)
- return true;
- return false;
- }
复制代码 利用逆时针排列向量积z分量小于0这一特点判别是否逆时针排列(由于图像坐标Y轴向下,所以向量积<0)。
在Log中查看一下顶点坐标:
- cout << polys[i] << endl;
复制代码- [249, 387;
- 232, 583;
- 440, 673;
- 495, 461]
复制代码
3.删除透视投影,对比给定标记
获取到标记4个顶点坐标后将要用getPerspectiveTransform和warpPerspective获取标记的正视图。用法如下:
- Mat m = getPerspectiveTransform(_poly, _rect);
- Mat temp;
- warpPerspective(origin, temp, m, Size(700, 700));
复制代码 由于getPerspectiveTransform接受的是vector<oint2f>型参数,要把vector<oint>的poly转成vector<oint2f>型的_poly。
同样定义一个逆时针顶点的矩形域来作为变换顶点产生投影矩阵m。
- vector<Point2f> _rect;
- _rect.push_back(Point2f(0, 0));
- _rect.push_back(Point2f(0, 700));
- _rect.push_back(Point2f(700, 700));
- _rect.push_back(Point2f(700, 0));
复制代码 利用warpPerspective函数把标记区域投影到一个700x700的矩形图中,如果之前的处理正确的话你应该获得了如下的视图之一:
出去边缘留白,有用的信息在中心5x5的矩形中,对比给定标记的信息即可得知是否为所需标记和其正确方向。流程如下:灰度化--阈值化--解码--对比--旋转--对比--找到正确方向 - cvtColor(temp, temp, CV_RGBA2GRAY);
- threshold(temp, temp, 125, 255, CV_THRESH_BINARY | CV_THRESH_OTSU);
- //解码
- Mat ecode = Mat(Size(5, 5), CV_8U);
- Mat sub;
- Rect rect;
- for (int i = 0; i < 5; i++){
- for (int k = 0; k < 5; k++){
- rect = Rect(100 + k * 100, 100 + i * 100, 100, 100);
- sub = temp(rect);
- ecode.at<uchar>(i, k) = countNonZero(sub)>5000*ratio ? 1 : 0; //用countNonZero(Mat)将白色区域编码为1,黑色区域为0.
- }
- }
- int index = 0;
- bool found = false;
- for (int i = 0; i < 4; i++){
- if (equals(markCode, ecode)){
- index = (4 - i) % 4;
- found = true;
- break;
- }
- else
- ecode = getRotateMat(ecode);
- }
- if (!found) continue;
复制代码 如果这个就是正确的标记的话,经过3次旋转一定会有一个完全相等的状态!最后就是要把检测到的标记直观的绘制出来: - Point A = Point(polys[j][index].x/ratio,polys[j][index].y/ratio);
- Point B = Point(polys[j][(index+1)%4].x/ratio,polys[j][(index+1)%4].y/ratio);
- Point C = Point(polys[j][(index+2)%4].x/ratio,polys[j][(index+2)%4].y/ratio);
- Point D = Point(polys[j][(index+3)%4].x/ratio,polys[j][(index+3)%4].y/ratio);
复制代码 效果图:
如果按照《深入理解OpenCV 实用计算机视觉项目解析》这本书的流程,接下来就是AR场景渲染了。我在Android上用OpenGL试了一下,简单的画了个立方体,效果如下: 或者像我一开始给出的效果图那样,对相机的每一帧进行这样从处理,从而实现实时AR渲染。流程如下: 相机内参数标定--实时外参数计算--根据内参数、外参数矩阵改变GL的投影和模型矩阵 不过这些都是题外话了,说不定以后有机会能细说 。
事实上,这里才是关键问题所在:在嵌入式平台(android、ios或其他)上,这种低成本的图像处理真的可行吗? 之前说到实验平台是VS2015+opencv2.4.13,在楼主i7-4720+GTX960M的硬件下,以上未经优化的代码跑一边大概需要100ms左右。如果输入图像拥有多个目标标记或者需要检测多种标记,再或者需要24FPS实时处理,这样还是远远不够的。 换到Android上又如何呢?楼主的实验平台为Nexus5X,骁龙808+Adreno 418 GPU,测试成绩如下: 1920x1080分辨率下,大部分时间全花在寻找标记上,平均用时177ms,平均帧数5FPS,根本是不可用的状态。在接收原图片时进行缩放降低像素是一种解决办法,代价是顶点坐标会更不准确(应该用亚像素角点)。那么还有其他解决方案吗? OpenCV有GPU加速的方案,但是需要CUDA。OpenCL呢?早在android4.3 google就已经移除了OpenCL相关的so。注意到以上算法中含有大量的循环运算和循环查找,异步计算仍然是最终方案。幸好android上还有RenderScript,运算结果如下: 貌似稍微快了一点呢 。 总结一下:嵌入式做视觉处理嘛,低成本就意味着高优化难度,OpenCV还是留着验证算法的好。 |