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);    }

分别把灰度图像和自适应阈值化图像保存至ImageGrayImageAdaptiveBinary

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

阅读全文
0 0
原创粉丝点击