NDK 开发实战 - 微信公众号二维码检测

红橙Darren
• 阅读 1741

关于二维码识别,我们一般都是用的 Zxing 或者 Zbar ,但它们的识别率其实不是很高,有些情况下是失灵的,比如下面这两张图:

NDK 开发实战 - 微信公众号二维码检测

NDK 开发实战 - 微信公众号二维码检测

使用开源库 Zxing 扫描以上两张二维码,有一张死活不识别。使用微信是可以的,大家可以用支付宝试试(不行),那碰到这种情况到底该怎么办呢?哈哈,这次终于有用武之地了,我们琢磨着来优化一把。

我们在微信公众号都用过这么一个功能,长按一张图片,如果该图片包含有二维码,会弹出识别图中二维码,如果该图片不含有二维码,则不会弹出识别二维码这个选项。说到这里我们大概应该知晓了,识别二维码其实分为两步,**第一步是发现截取二维码区域,第二步是识别截取到的二维码区域。**那么 zxing 和支付宝到底是哪一步出了问题呢?首先我们来看一下第一步发现截取二维码区域。

NDK 开发实战 - 微信公众号二维码检测

上图是一张常用的二维码事例图,有三个比较重要的区域,分别是左上,右上和左下,我们只要能找到这三个特定的区域,就能判定图片中包含有二维码。接下来我们来分析一下思路:

1. 对其进行轮廓查找 2. 对查找的到的轮廓进行初步过滤 3. 判断是否符合二维码的特征规则 4. 截取二维码区域 5. 识别二维码

//  判断 X 方向上是否符合规则
bool isXVerify(const Mat& qrROI){
    ... 代码省略
    // 判断 x 方向从左到右的像素比例
    // 黑:白:黑:白:黑 = 1:1:3:1:1
}

//  判断 Y 方向上是否符合规则
bool isYVerify(const Mat& qrROI){
    ... 代码省略
    // y 方向上也可以按照 isXVerify 方法判断
    // 但我们也可以适当的写简单一些
    // 白色像素 * 2 < 黑色像素 && 黑色像 < 4 * 白色像素 
}

int main(){
    Mat src = imread("C:/Users/hcDarren/Desktop/android/code1.png");

    if (!src.data){
        printf("imread error!");
        return -1;
    }
    imshow("src", src);

    // 对图像进行灰度转换
    Mat gary;
    cvtColor(src, gary, COLOR_BGR2GRAY);
    // 二值化
    threshold(gary, gary, 0, 255, THRESH_BINARY | THRESH_OTSU);
    imshow("threshold", gary);
    // 1. 对其进行轮廓查找
    vector<vector<Point> > contours;
    findContours(gary, contours, RETR_LIST, CHAIN_APPROX_SIMPLE);

    for (int i = 0; i < contours.size(); i++)
    {
        // 2. 对查找的到的轮廓进行初步过滤
        double area = contourArea(contours[i]);
        // 2.1 初步过滤面积 7*7 = 49
        if (area < 49){
            continue;
        }

        RotatedRect rRect = minAreaRect(contours[i]);
        float w = rRect.size.width;
        float h = rRect.size.height;
        float ratio = min(w, h) / max(w, h);
        // 2.2 初步过滤宽高比大小
        if (ratio > 0.9 && w< gary.cols/2 && h< gary.rows/2){
            Mat qrROI = warpTransfrom(gary, rRect);
            // 3. 判断是否符合二维码的特征规则
            if (isYVerify(qrROI) && isXVerify(qrROI)) {
                drawContours(src, contours, i, Scalar(0, 0, 255), 4);
            }
        }
    }

    imshow("src", src);
    imwrite("C:/Users/hcDarren/Desktop/android/code_result.jpg", src);

    waitKey(0);
    return 0;
} 

NDK 开发实战 - 微信公众号二维码检测

**代码是非常简单的,关键是我们要善于学会去分析,多多培养解决问题的能力,只要知道实现思路,其他一切都不是问题了。**那么有意思的就来了,当扫描第二张图的时候,我们发现死活都识别不了。那么细心的同学可能明白了,我们上面的代码是按照正方形的特征来识别的,而第二张图是圆形的特征,因此 Zxing 无法识别也是正常的,因为咱们在写代码的时候根本没考虑这么个情况。那么我们怎么才能做到识别圆形的特征呢?考验我们的时候到了,我们能想到三种解决方案:

1. 再写一套识别圆形特征的代码 2. 借鉴人脸识别的方案,采用训练样本的方式识别 3. 换一种检查方案,只写一套代码

人脸识别在下期文章中会写到,训练样本的方式比较麻烦,如果之前没接触过,那么需要一定的时间成本,但这种方案应该是最好的。再写一套圆形识别的代码,感觉维护困难,作为一个有灵魂的工程师总觉得别扭。那这里我们就采用第三种方案了,其实知识点也就那么多,还是那句话多培养我们分析解决问题的能力

我们仔细观察,他们其实还是有很多共同点,我们对其进行轮廓筛选的时候会发现,都是一个大轮廓里面套两个小轮廓。具体流程如下:

1. 对其进行轮廓查找 2. 对查找的到的轮廓进行初步过滤 3. 判断是否是一个大轮廓套两个小轮廓且符合特征规则(面积比例判断) 4. 截取二维码区域 5. 识别二维码

extern "C"
JNIEXPORT jobject JNICALL
Java_com_darren_ndk_day76_MainActivity_clipQrBitmap(JNIEnv *env, jobject instance, jobject bitmap) {
    Mat src;
    cv_helper::bitmap2mat(env, bitmap, src);

    // 对图像进行灰度转换
    Mat gary;
    cvtColor(src, gary, COLOR_BGR2GRAY);

    // 二值化
    threshold(gary, gary, 0, 255, THRESH_BINARY | THRESH_OTSU);

    // 1. 对其进行轮廓查找
    vector<Vec4i> hierarchy;
    vector<vector<Point> > contours;
    vector<vector<Point> > contoursRes;
    /*
     参数说明:https://blog.csdn.net/guduruyu/article/details/69220296
        输入图像image必须为一个2值单通道图像
        contours参数为检测的轮廓数组,每一个轮廓用一个point类型的vector表示
        hiararchy参数和轮廓个数相同,每个轮廓contours[ i ]对应4个hierarchy元素hierarchy[ i ][ 0 ] ~hierarchy[ i ][ 3 ],
            分别表示后一个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引编号,如果没有对应项,该值设置为负数。
        mode表示轮廓的检索模式
            CV_RETR_EXTERNAL 表示只检测外轮廓
            CV_RETR_LIST 检测的轮廓不建立等级关系
            CV_RETR_CCOMP 建立两个等级的轮廓,上面的一层为外边界,里面的一层为内孔的边界信息。如果内孔内还有一个连通物体,这个物体的边界也在顶层。
            CV_RETR_TREE 建立一个等级树结构的轮廓。具体参考contours.c这个demo
        method为轮廓的近似办法
            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(gary, contours, hierarchy, CV_RETR_TREE, CHAIN_APPROX_NONE, Point(0, 0));
    int tCC = 0; // 临时用来累加的子轮廓计数器
    int pId = -1;// 父轮廓的 index
    for (int i = 0; i < contours.size(); i++) {
        if (hierarchy[i][2] != -1 && tCC == 0) {
            pId = i;
            tCC++;
        } else if (hierarchy[i][2] != -1) {// 有父轮廓
            tCC++;
        } else if (hierarchy[i][2] == -1) {// 没有父轮廓
            tCC = 0;
            pId = -1;
        }
        // 找到了两个子轮廓
        if (tCC >= 2) {
            contoursRes.push_back(contours[pId]);
            tCC = 0;
            pId = -1;
        }
    }
    // 找到过多的符合特征轮廓,对其进行筛选
    if (contoursRes.size() > FEATURE_NUMBER) {
        contoursRes = filterContours(gary, contoursRes);
    }

    // 没有找到符合的条件
    if (contoursRes.size() < FEATURE_NUMBER) {
        return NULL;
    }

    for (int i = 0; i < contoursRes.size(); ++i) {
        drawContours(src, contoursRes, i, Scalar(255, 0, 0), 2);
    }

    // 裁剪二维码,交给 zxing 或者 zbar 处理即可

    cv_helper::mat2bitmap(env, src, bitmap);

    return bitmap;
} 

NDK 开发实战 - 微信公众号二维码检测

开发中我们最喜欢做的就是拿过来直接用,但最好还是明白其中的原理,因为我们无法断定开发中会出什么幺蛾子。像微信这样的大厂自然得自己这一套,其实好的框架能够拿过来优化优化,个人认为就已经差不多了。当然以上写法在某些特定场景下,可能还是会存在些许漏洞,这就靠我们不断的去琢磨优化了。

视频地址:pan.baidu.com/s/1m7Epc4TV… 视频密码:5g3z

本文转自 https://juejin.cn/post/6844903807839633422,如有侵权,请联系删除。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Karen110 Karen110
3年前
一篇文章带你了解JavaScript日期
日期对象允许您使用日期(年、月、日、小时、分钟、秒和毫秒)。一、JavaScript的日期格式一个JavaScript日期可以写为一个字符串:ThuFeb02201909:59:51GMT0800(中国标准时间)或者是一个数字:1486000791164写数字的日期,指定的毫秒数自1970年1月1日00:00:00到现在。1\.显示日期使用
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
Docker 部署SpringBoot项目不香吗?
  公众号改版后文章乱序推荐,希望你可以点击上方“Java进阶架构师”,点击右上角,将我们设为★“星标”!这样才不会错过每日进阶架构文章呀。  !(http://dingyue.ws.126.net/2020/0920/b00fbfc7j00qgy5xy002kd200qo00hsg00it00cj.jpg)  2
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这