在写此博客之前,刚好了解到百度现在全面禁止使用React/React Native,而转型Vue.js等其他的开源库。因为Facebook的协议Facebook BSD+PATENTS License对于专利权具有防御性,目前Facebook 工程师 Adam Wolff 在 FB 博客发文称,FB 将在下周把 React、Jest、Flow、Immutable.js 四个产品的开源协议改成 MIT。前一段时间也通过Vue.js开源框架写了今日头条首页的内容仿今日头条,而Vue.js除了只支持浏览器IE 8以上瑕疵之外,确实也比React的开源库具有强大的优势.

最近公司项目中又添加了一些语音识别文字识别身份证等识别功能需求,而项目中使用的是大厂的SDK.但是因为在前几年的时候对这方面只是有过简单的了解,并未深入去探索,因此现在又重新去查看了OpenCV的内容和资料.OpenCV可以实现哪些功能呢?

敬请查看视频

本项目模拟名片全能王简单功能

以下GIF为demo所要实现的内容

项目实现

1. OpenCV的安装

现在官网已经友好的为我们集成了各个平台所要使用的SDK,本项目使用的是iOS framework. 但是在项目中,我们所使用的模板中包括xfeatures2d, 此对象在OpenCV早起的版本中会被集成当中,但是在OpenCV3.0以后,被移动到opencv_contrib中.opencv_contrib库表明在此内的模板API还没有稳定,并且它们也没有很好的被测试. 你可以通过opencv_contrib中的方式自己进行集成模块,也可以下载我已经集成过xfeatures2dopencv2.framework.

2. 开发准备

2.1 本项目完整项目可以点此下载地址.

在TARGETS -> General -> Linked Frameworks andLibraries 中添加如下framework: opencv2.frameworkUIKit.frameworkQuartzCore.framework
Photos.frameworkCoreVideo.frameworkCoreMedia.framework
CoreGraphics.frameworkAVFoundation.frameAssetsLibrary.framwork

2.2 本项目并未使用xfeatures2d模块.若你要后期使用,可以将下载好的项目中的opencv2.framework替换为xfeatures2d的opencv2.framework即可.

3. 实现

在下载的初始项目中,运行后我们能够了解到只存在一个实时视频的界面,并未实现其他的处理,现在我们开始实现具体细节。

3.1 创建对象

ViewController.m中,导入以下的头文件,并且创建如下两个变量.

#import "BusinessCardDetector.hpp"
#import "BusinessCard.hpp"
@interface ViewController ()<CvVideoCameraDelegate>
{
    //1.
    BusinessCardDetector *businessCardDetector;
    //2.
    std::vector<BusinessCard> detectedBusinessCards;
}
  1. 指针指向viewDidLoad中创建的对象;
  2. std::vector:C++中的一种数据结构,相当于保存BusinessCard的动态的数组

processImage:(cv::Mat &)mat方法中,通过businessCardDetector对象方法将获取到的视频照片信息进行处理。

- (void)processImage:(cv::Mat &)mat {
    switch     (self.videoCamera.defaultAVCaptureVideoOrientation) {
        case AVCaptureVideoOrientationLandscapeLeft:
        case AVCaptureVideoOrientationLandscapeRight:
            // The landscape video is captured upside-down.
            // Rotate it by 180 degrees.
            cv::flip(mat, mat, -1);
            break;
        default:
            break;
    }
    // Detect businessCardDetector
    businessCardDetector->detect(mat,     detectedBusinessCards,DETECT_RESIZE_FACTOR, true);
    businessCardDetector->getMask().copyTo(mat);
}

3.2 图像轮廓

在对图像处理的时候,我们会将图片进行高斯模糊、灰度值、二值化以及图形学腐蚀等操作.详细见如下代码;

    void BusinessCardDetector::detect(cv::Mat &image, std::vector<BusinessCard> &businessCard, double resizeFactor, bool draw)
{
    //1.
    businessCard.clear();

    //2.
    cv::resize(image, resizedImage, cv::Size(), resizeFactor,resizeFactor, cv::INTER_AREA); //缩放比列

    //3.
    cv::GaussianBlur(resizedImage, resizedImage,cvSize(1, 1),0);

    //4.
    cv::cvtColor(resizedImage, resizedImage, CV_RGB2GRAY);

    //5.
    cv::Canny(resizedImage, edges, 130, 150);

    //6.
    cv::threshold(edges, edges, 0, 255, CV_THRESH_BINARY);

    //7.
    cv::findContours(edges, contours,cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);

    //8.
    drawContours(image, contours, -1, DRAW_RECT_COLOR,2);
}
  1. 在每次进行图形处理时都先将此数组的内容进行清理;
  2. 将图片进行比例缩放,使用此方法(1)提高视频界面的清晰程度;(2)提高图形处理的性能;
  3. 高斯模糊,使图像中的每一个点都可以进行平滑处理;
  4. 图像进行灰度值处理,此方式为步骤5做准备;
  5. Canny函数进行图片的边缘检测,能够查出来图片中的所有的边缘;
  6. 对图片进行二值化,即使图像只包含黑白两色,能使图像更容易找到轮廓;
  7. 找出图像数据的全部外部轮廓。
  8. 将所有找出来的轮廓在原图数据上进行绘画出来。(注意注释processImage方法中的businessCardDetector->getMask().copyTo(mat);显示获取边缘edges).

比例缩放:

void resize( InputArray src, OutputArray dst,Size dsize, double fx = 0, double fy = 0,int interpolation = INTER_LINEAR );
src: 原始图像数据矩阵;
dst: 目标图像数据矩阵;
dsize: 目标图像的大小;
fx和fy: 对目标图片x和y的比例,当为0时,使用目标图片的size除去原始图片的cols和rows;

高斯模糊:

void GaussianBlur( InputArray src, OutputArray dst, Size ksize,double sigmaX, double sigmaY = 0,int borderType = BORDER_DEFAULT )
ksize: 高斯内核大小. 此值必须为基数。如果此数为0,则根据sigma进行计算;
sigmaX:表明高斯内核在水平方向上的标准偏差

Canny:

void Canny( InputArray image, OutputArray edges, double threshold1, double threshold2,  int apertureSize = 3, bool L2gradient = false)
image: 单通道输入图像;
edges: 单通道存储边缘的输出图像;
threshold1: 第一个阈值;
threshold2: 第二个阈值;
这二个阈值中当中的小阈值用来控制边缘连接,大的阈值用来控制强边缘的初始分割即如果一个像素的梯度大与上限值,则被认为是边缘像素,如果小于下限阈值,则被抛弃。如果该点的梯度在两者之间则当这个点与高于上限值的像素点连接时我们才保留,否则删除。

二值化:

double threshold( InputArray src, OutputArray dst,double thresh, double maxval, int type )
thresh:当前阈值;
maxVal:最大阈值,一般为255;
   THRESH BINARY:二进制阈值,。在运用该阈值类型的时候,先要选定一个特定的阈值量,比如:125,这样,新的阈值产生规则可以解释为大于125的像素点的灰度值设定为最大值255,灰度值小于125的像素点的灰度值设定为0。  (value>threshold?255:0)
   THRESH BINARY INV:反二进制阈值。设定一个初始阈值如125,则大于125的设定为0,而小于该阈值的设定为255。  (value>threshold?0:255)
   THRESH TRUNC:截断阈值。同样首先需要选定一个阈值,图像中大于该阈值的像素点被设定为该阈值,小于该阈值的保持不变。(例如:阈值选取为125,那小于125的阈值不改变,大于125的灰度值(230)的像素点就设定为该阈值)。   (value>threshold?threshold:value)
   THRESH TOZERO:阈值化为0。先选定一个阈值,像素点的灰度值大于该阈值的不进行任何改变;像素点的灰度值小于该阈值的,其灰度值全部变为0。   (value>threshold?value:0)
   THRESH TOZERO INV:反阈值化为0。原理类似于0阈值,但是在对图像做处理的时候相反,即:像素点的灰度值小于该阈值的不进行任何改变,而大于该阈值的部分,其灰度值全部变为0。  (value>threshold?0:value)
   例:threshold( src,dst,125,255,0);

所获得的效果如下:

Canny边缘检测画轮廓

3.3 画图形框

在以上效果图中会看到轮廓较小,这是因为使用了比例进行缩放.为了获取得到的名片边框,可以通过获取得到的轮廓进行判断。这是因为contours中是一些Point(点)的数组.因此通过每一个Point点数组得到每一个的矩形框.

我们不使用cv::boundingRect获取Rect矩形框.这是因为cv::boundingRect获取的矩形并不直接是名片大小,且不能获取不标准摆放的名片矩形。

通过对contours进行遍历,使用cv::minAreaRect方法对每一个点数组进行取RotatedRect矩形.然后进行大小比较,在大小为可接受范围则进行画轮廓.
BusinessCardDetector.cpp文件中,添加如下方法:

bool BusinessCardDetector::verifySizes(RotatedRect mr,double factor)
{

    int minW = 9.4 * 4.8 * pow(50, 2);
    int maxW = 9.4 * 5.8 * pow(180, 2);
    float minAspect = 9.4 / 5.8;
    float maxAspect = 9.4 / 4.8;

    int area = mr.size.height / factor * mr.size.width / factor;
    float r = MAX(mr.size.width, mr.size.height)*1.0 / MIN(mr.size.width, mr.size.height) ;

    if ((area > maxW || area < minW ) || (r < minAspect || r > maxAspect))
    {
        return  false;
    }
    return true;
}

此方法通过查询出目前的名片的最大宽度、高度和最小宽度、高度值,通过pow()方式,表示在视频界面中所可获取的远近距离,以及名片的最大宽高比同获取到的图像轮廓进行比较,当在所判断的范围内时,则是一张名片.

注: 此验证方式并不准确,可以通过机器学习进行数据训练匹配,也可以使用opencv的模板进行匹配

通过修改步骤9,进行画一张在缩小图像中的轮廓,代码如下:

  std::vector<std::vector<cv::Point>> verified_contours ;
verified_contours.clear();
for (std::vector<cv::Point> contour : contours) {
RotatedRect rotatedRect = cv::minAreaRect(contour);
if (verifySizes(rotatedRect,resizeFactor) && rotatedRect.size.width != 0 && rotatedRect.size.height != 0) {
    verified_contours.push_back(contour);
     break;
       }
   }
   drawContours(image, verified_contours,-1,DRAW_RECT_COLOR,3);

效果如下图:

轮廓

3.4 获取原始图像轮廓

通过已修改的步骤9,我们可以获取到rotatedRect的中心点、宽高以及4个顶点(左下,左上,右上,右下)的坐标.我们将中心点、宽高都进行等比例还原,然后储存在businessCard对象中,将4个顶点存储在当前对象的临时数组中.代码如下所示:

this->temp[4] = {};
    std::vector<std::vector<cv::Point>> verified_contours ;
    verified_contours.clear();
    for (std::vector<cv::Point> contour : contours)     {
        RotatedRect rotatedRect = cv::minAreaRect(contour);
        if (verifySizes(rotatedRect,resizeFactor) && rotatedRect.size.width != 0 && rotatedRect.size.height != 0) {
            rotatedRect.center.x /= resizeFactor;
            rotatedRect.center.y /= resizeFactor;
            rotatedRect.size.width /= resizeFactor;
            rotatedRect.size.height /= resizeFactor;
            BusinessCard b = BusinessCard(cv::Mat(rotatedRect.size.width,rotatedRect.size.height,CV_8UC2),rotatedRect.center);
            this->setCenterX(rotatedRect.center.x);
            this->setCenterY(rotatedRect.center.y);
            Point2f vertices[4];
            rotatedRect.points(vertices);
            for (int i = 0; i < 4; i++)
              {
                  Point2f p = vertices[i];
                  this->temp[i] = p;
                }
            businessCard.push_back(b);      
            break;
       }
}

可以通过等比例处理过rotatedRect中的boundingRect()方法,使用 cv::rectangle()方式进行画矩形(此方式同样不推荐)

rotatedRect.points此方式获取的同样为rectangle的坐标,也就是说获取的并非只是轮廓的大小,而是轮廓的最小规则矩形.

将获取的宽高、中心点通过BusinessCard对象进行保存到businessCard数组中,我们可以在ViewController.m文件中进行处理.

3.5 处理动画

当获取到4个顶点以及中心点时,我们就可以通过CAShapeLayer进行动画处理.

3.5.1 处理CAShapeLayer

当我们获取到4个顶点时,我们可以根据中心点的进行4个顶点的判断处理,分别获取到CAShapeLayer的左下、左上、右上、右下的坐标.代码处理如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
if(detectedBusinessCards.size()> 0) {
BusinessCard b = detectedBusinessCards[0];
//获取中心点
CGPoint centerPoint = CGPointMake(businessCardDetector->getCenterX()*1.0/mat.cols*width,businessCardDetector->getCenterY()*1.0/mat.rows*height);
//获取到4个顶点
float point1x = businessCardDetector->temp[0].x*1.0/mat.cols*width;
float point1y = businessCardDetector->temp[0].y*1.0/mat.rows*height;
CGPoint point1 = {point1x,point1y};
float point2x = businessCardDetector->temp[1].x*1.0/mat.cols*width;
float point2y = businessCardDetector->temp[1].y*1.0/mat.rows*height;
CGPoint point2 = {point2x,point2y};
float point3x = businessCardDetector->temp[2].x*1.0/mat.cols*width;
float point3y = businessCardDetector->temp[2].y*1.0/mat.rows*height;
CGPoint point3 = {point3x,point3y};
float point4x = businessCardDetector->temp[3].x*1.0/mat.cols*width;
float point4y = businessCardDetector->temp[3].y*1.0/mat.rows*height;
CGPoint point4 = {point4x,point4y};
// self.bordermask.lb self.bordermask.lt self.bordermask.rt self.bordermask.rb
NSArray * array = @[NSStringFromCGPoint(point1),
NSStringFromCGPoint(point2),
NSStringFromCGPoint(point3),
NSStringFromCGPoint(point4)];
//规划四个顶点
for (NSString * pointStr in array) {
CGPoint point = CGPointFromString(pointStr);
CGFloat widthOffset = point.x - centerPoint.x;
CGFloat heightOffset = point.y - centerPoint.y;
if (widthOffset > 0.0) {
if (heightOffset > 0.0) {
self.bordermask.rb = point;
}else
{
self.bordermask.rt = point;
}
}else
{
if (heightOffset > 0.0) {
self.bordermask.lb = point;
}else
{
self.bordermask.lt = point;
}
}
}

因为opencv的处理全部都在子线程中,所以当进行CAShapeLayer动画时,我们要在主线程中对CALayer进行处理.
至此,项目中内容全部完成.

注:项目demo中包括一些图形学的腐蚀和膨胀我并未作说明,腐蚀和膨胀你可以查看学习OpenCV(中文版)进行理解.

查看资料包括以下内容