阅读 1279

如何做出比钉钉更优秀长按识别二维码功能

Zxing扫码二维码源码优化

背景

公司有个长按识别图片二维码的功能,但是基于原生zxing的识别存在以下几个问题

存在问题

1. 图片二维码区域清晰但是占用面积过小时无法识别,比如以下这张,钉钉无法识别,微信可以识别


2. 图片二维码存在过多噪点时无法识别,比如以下这张,网上的开源库无法识别,钉钉微信都可以识别

解决方法

不传整张bitmap,只传view在屏幕上的截屏

这个方法可以解决二维码太小无法识别的问题,但是存在一个问题是如果用户没有滑动到二维码区域也是无法识别。在第二个问题没出现之前尝试了这个方法,但是第二个问题后放弃了这个方案。

源码优化+传入与宽度与屏幕相同高度与bitmap原图等比例的图片

尝试了一轮在应用层的优化无果后,只能在源码着手。于是查看了zxing的源码。至于为什么要传入等比例的图片是因为让图片在一定尺寸内更加清晰可见以兼容小屏手机生成的二维码无法识别的问题。

扫码区域过小问题解决

  • 查看源码发现,用户传入的bitmap转成image后会走到FinderPatternFinder
public class FinderPatternFinder {

   …………
    final core.qrcode.detector.FinderPatternInfo find(Map<DecodeHintType, ?> hints) throws NotFoundException {
        boolean tryHarder = hints != null && hints.containsKey(DecodeHintType.TRY_HARDER);
        int     maxI      = image.getHeight();
        int     maxJ      = image.getWidth();
        // We are looking for black/white/black/white/black modules in
        // 1:1:3:1:1 ratio; this tracks the number of such modules seen so far

        // Let's assume that the maximum version QR Code we support takes up 1/4 the height of the
        // image, and then account for the center being 3 modules in size. This gives the smallest
        // number of pixels the center could be, so skip this often. When trying harder, look for all
        // QR versions regardless of how dense they are.
        //TODO 关键代码
        int iSkip = (3 * maxI) / (4 * MAX_MODULES);
        if ( iSkip < MIN_SKIP || tryHarder ) {
            iSkip = MIN_SKIP;
        }
…………
复制代码

// Let's assume that the maximum version QR Code we support takes up 1/4 the height of the // image, and then account for the center being 3 modules in size. This gives the smallest // number of pixels the center could be, so skip this often. When trying harder, look for all

翻译过来是

//假设我们支持的最大版本QR代码占用了1/4的高度。

//图像,然后说明中心是3个模块的大小。这是最小的

//中心可以是多少像素,所以经常跳过这个。当你更努力的时候,寻找所有

//QR版本,不管它们有多密集。
复制代码

看到这里我把int iSkip = (3 * maxI) / (4 * MAX_MODULES);改成int iSkip = (3 * maxI) / (8 * MAX_MODULES);。神奇的发现可以识别小图了。

噪点过多问题解决

这个问题就不能那么侥幸了,真的要理清整个源码的过程。经过查看,发现修改的还是同一个类。

 /**
     * @return the 3 best {@link core.qrcode.detector.FinderPattern}s from our list of candidates. The "best" are
     * those that have been detected at least {@link #CENTER_QUORUM} times, and whose module
     * size differs from the average among those patterns the least
     * @throws NotFoundException if 3 such finder patterns do not exist
     */
    private core.qrcode.detector.FinderPattern[] selectBestPatterns() throws NotFoundException {

        int startSize = possibleCenters.size();

        if ( startSize < 3 ) {
            // Couldn't find enough finder patterns
            throw NotFoundException.getNotFoundInstance();
        }

        // Filter outlier possibilities whose module size is too different
        if ( startSize > 3 ) {
            // But we can only afford to do so if we have at least 4 possibilities to choose from
            double totalModuleSize = 0.0;
            double square          = 0.0;
            for ( core.qrcode.detector.FinderPattern center : possibleCenters ) {
                float size = center.getEstimatedModuleSize();
                totalModuleSize += size;
                square += size * size;
            }
            double average = totalModuleSize / startSize;
            float  stdDev  = ( float ) Math.sqrt(square / startSize - average * average);

            Collections.sort(possibleCenters, new FurthestFromAverageComparator(( float ) average));

            float limit = Math.max(0.2f * ( float ) average, stdDev);

            for ( int i = 0 ; i < possibleCenters.size() && possibleCenters.size() > 3 ; i++ ) {
                core.qrcode.detector.FinderPattern pattern = possibleCenters.get(i);
                if ( Math.abs(pattern.getEstimatedModuleSize() - average) > limit ) {
                    possibleCenters.remove(i);
                    i--;
                }
            }
        }

        if ( possibleCenters.size() > 3 ) {
            // Throw away all but those first size candidate points we found.

            float totalModuleSize = 0.0f;
            for ( core.qrcode.detector.FinderPattern possibleCenter : possibleCenters ) {
                totalModuleSize += possibleCenter.getEstimatedModuleSize();
            }

            float average = totalModuleSize / possibleCenters.size();

            Collections.sort(possibleCenters, new CenterComparator(average));
            
            possibleCenters.subList(3, possibleCenters.size()).clear();//取比较后的前三个关键点

        }

        return new core.qrcode.detector.FinderPattern[]{
                possibleCenters.get(0),
                possibleCenters.get(1),
                possibleCenters.get(2)
        };
    }
复制代码

@从候选列表中返回3个最佳{@link core.qrcode.detector.FinderPattern}s。“最好的”是那些至少被检测到{@link#CENTER_QUORUM}次,并且其模块 在这些图案中,大小与平均值相差最小 如果不存在3个查找模式,@抛出

所有的关键点都会走这个地方,过多的会根据zxing自己提供的方法过滤掉·,过少则会抛出异常。至于什么是关键点,在图像识别里面是根据二维码顶端黑白区间的比例确定的,所以可能会有误差。

  回到问题,识别噪点过多的图片在文章开头有,识别会发现返回的是以下的坐标

//手动模拟,方便测试
    PointF       pointF    = new PointF(187.5f, 391.5f);
        PointF       pointF1   = new PointF(693.5f,391.5f);
        PointF       pointF2   = new PointF(655.0f,606.0f);
        PointF       pointF3   = new PointF(325.0f,859.0f);
        PointF       pointF4   = new PointF(187.5f,897.5f);
复制代码

发现这里有5个点,看回源码

 possibleCenters.subList(3, possibleCenters.size()).clear();//取比较后的前三个关键点
复制代码

这里zxing帮我们过来出来的是前三个点,明显这个是不対的,其实根据我们肉眼观察。肯定有两个点事x轴接近相等,有两个点y轴接近相等。这样筛选出来的点应该是1、2、5

于是自己做了一次筛选

  //possibleCenters.subList(3, possibleCenters.size()).clear();//取比较后的前三个关键
  
  CustomSort(possibleCenters.size())
复制代码
private void CustomSort(int startSize) {
        centerPos.clear();
        if ( startSize != 0 ) {
            possibleCenterCopy.clear();
            int     size       = possibleCenters.size();
            boolean isNotFindX  = true;
            boolean isNotFindY = true;
            for ( int i = 0 ; i < size ; i++ ) {
                for ( int i1 = i + 1 ; i1 < size ; i1++ ) {

                    if ( isNotFindX && isQrCodeRate(possibleCenters.get(i).getX(), possibleCenters.get(i1).getX()) ) {
                        centerPos.add(i);
                        centerPos.add(i1);
                        isNotFindX = false;
                    }
                    if ( isNotFindY && isQrCodeRate(possibleCenters.get(i).getY(), possibleCenters.get(i1).getY()) ) {
                        centerPos.add(i);
                        centerPos.add(i1);
                        isNotFindY = false;
                    }
                }

            }

//            Log.i("---","centerPos====>"+centerPos.size());
            for ( Integer center : centerPos ) {
                possibleCenterCopy.add(possibleCenters.get(center));
            }

            //如果找不到三个关键点,走以前的逻辑
            if ( possibleCenterCopy.size() < 3 ) {
                possibleCenters.subList(3, possibleCenters.size()).clear();//取比较后的前三个关键点
            } else {
                possibleCenters.clear();
                //如果噪点过多。取比较后的前三个关键点
                if ( possibleCenterCopy.size() > 3 ) {
                    possibleCenterCopy.subList(3, possibleCenters.size()).clear();
                }
                possibleCenters.addAll(possibleCenterCopy);
            }
        }
    }

复制代码

经过测试发现,有时候两个x存在一些误差,但是理论上还是关键点。所以做了以下过滤

  //判断是否是关键点,允许有误差
    private boolean isQrCodeRate(float x, float x1) {
        float errorSize = 0.5f;
        return Math.abs(x - x1) <= errorSize;
    }
复制代码

至此,上面的两张图片都可以识别了。

扫码自动放大摄像头

其实我们看微信还有支付宝等等扫码都有一个摄像头自动放大的功能,而这个功能在网上也有一些应用层的方案但都是不可行的,链接如下。

因为在应用层的回调是识别出来二维码才回调的,所以在二维码识别出来后再放大其实毫无意义。所以要做到微信那样能边缘检测放大摄像头的需要再源码修改就是selectBestPatterns这个方法。这部分我暂时没有做,但是根据已有的条件其实是可以做到的。比如我想的策略是:

当检测到两个垂直或者水平的点时放大摄像头,如果超过5次没检测到变回原来的大小
        //TODO 关键点检测关键代码。后续可以添加自己的策略自动放大缩小摄像头
//        if ( startSize != 0 ) {
//            for ( int i = 0 ; i < startSize ; i++ ) {
//                Log.i("----", "关键点检测关键代码====" + i + "-----" + possibleCenters.get(i).toString()
//                        + possibleCenters.get(i).toString() + possibleCenters.get(i).getEstimatedModuleSize()
//                );
//
//            }
//            Log.i("----", "关键点检测关键代码------------------------------------------------");
//        }
复制代码

只要在这里做一个回调给到扫码的activity我相信这个需求是可以实现的。

不足

两个for循环的筛选感觉效率没有达到最优的性能,此处还望大佬可以指点优化一下。以下是单元测试代码

   /**
     * 关键点检测代码
     */
    @Test
    public void sortPoint(){
        List<PointF> pointFS   =new ArrayList<>();
        PointF       pointF    = new PointF(187.5f, 391.5f);
        PointF       pointF1   = new PointF(693.5f,391.5f);
        PointF       pointF2   = new PointF(655.0f,606.0f);
        PointF       pointF3   = new PointF(325.0f,859.0f);
        PointF       pointF4   = new PointF(187.5f,897.5f);
        pointFS.add(pointF);
        pointFS.add(pointF1);
        pointFS.add(pointF2);
        pointFS.add(pointF3);
        pointFS.add(pointF4);


        //方法1
        boolean isNotFindX  = true;
        boolean isNotFindY = true;
        Set<Integer>  centerPos =new HashSet<>();
        int size = pointFS.size();
        for ( int i = 0 ; i < size ; i++ ) {
            for ( int i1 = i+1 ; i1 < size ; i1++ ) {
                if ( isNotFindX && isQrCodeRate(pointFS.get(i).x, pointFS.get(i1).x) ) {
                    centerPos.add(i);
                    centerPos.add(i1);
                    isNotFindX = false;
                }
                if ( isNotFindY && isQrCodeRate(pointFS.get(i).y, pointFS.get(i1).y) ) {
                    centerPos.add(i);
                    centerPos.add(i1);
                    isNotFindY = false;
                }
            }
        }
        System.out.println("关键点坐标centerPos======>"+"----"+centerPos.size());

        for ( Integer centerPo : centerPos ) {
            System.out.println("关键点坐标centerPos======>"+ centerPo+"----"+centerPos.size());
        }
    }

    //判断是否是关键点,允许有误差
    private boolean isQrCodeRate(float x, float x1) {
        float errorSize = 0.5f;
        return Math.abs(x - x1) <= errorSize;
    }

复制代码

最后

 几个月没写博客,第一个原因是换了工作比较忙,还有就是中间摔伤了脚修养了一段时间想明白了一些事情不想在一条不能实现自己梦想的道路上越走越远,还有不能存在太多的幻想。当然这些话是对自己说的

 最后想不能只是不断的在网络上摄取社区的知识,也应该回馈一下自己的研究成果。


最开始坚持的地方,记录学习与生活的点点滴滴