Zxing扫码库优化思路

0a653bb4b61d496fea829d4d938db186.png

/   今日科技快讯   /

近日,抖音直播发布《抖音关于打击直播诈骗黑色产业链的公告》。公告称,为进一步保障用户及平台安全,抖音下阶段将重点推进MCN治理、帐号治理等专项行动。同时,将于即日起开展针对直播黑产的专项打击。对于违规情节严重或涉及违法犯罪行为的主播及MCN/公会,平台会主动上报行业黑名单并将相关情况报送公安机关等有关部门。

/   作者简介   /

本篇文章来自红鲤鱼鲤驴与驴的投稿,文章主要分享了他对Zxing扫码库优化的思路,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

红鲤鱼鲤驴与驴的博客地址:

https://juejin.cn/user/2960298998247624

/   Zxing库结构   /

e5f894c92c9e93d2abd69ababbbd7b55.png

优化之前先来简单介绍一下Zxing库的结构(如上图):

我们知道Zxing库支持生成和识别多种码型,对应码型的代码逻辑封装在各个包下(如一维码的逻辑封装的oned下,pdf417码型在pdf417包下)。


我们本次优化的QR_Code(最常见的二维码)则是在qrcode包下。生成逻辑在QRCodeWriter类中,识别逻辑在QRCodeReader类中。


扫码页:CaptureActivity


扫描流程

camera预览帧 -> 二值化(将图像转化成01矩阵1⃣以做后续处理) -> 扫描定位点 -> 畸变校正-> 识别内容(编码的逆运算)

/   识别篇   /

从图像的角度

我们应该保证相机获取到的图像足够清晰,因此可以调整camera参数来获取更清晰的图像。举两个🌰。


曝光度调节


根据光线传感器的lux值,调节相机曝光度。具体来说就是环境亮度越大,设置相机曝光度越低,避免图像过度曝光;环境亮度越小,设置相机曝光度越大,避免图像过暗。注册光线传感器监听,监听建议写在扫码页面的CaptureActivity中。

onCreate() 进行注册
//第一步:获取 SensorManager 的实例
sensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
//第二步:获取 Sensor 传感器类型
Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);

//第四步:注册 SensorEventListener
sensorManager.registerListener(listener,sensor,SensorManager.SENSOR_DELAY_UI);

 onDestroy()解绑
if (sensorManager!=null) {
    sensorManager.unregisterListener( listener );
}

监听器:

//第三步:对传感器信号进行监听
private SensorEventListener listener = new SensorEventListener() {
    @Override
    public void onSensorChanged(SensorEvent event) {
        float lux = event.values[0];//获取光线强度
        CameraManager cameraManager = getCameraManager();
        Camera camera = cameraManager.getCamera();
        Camera.Parameters parameters = camera.getParameters();
        if (lux < 50) {
            parameters.setExposureCompensation(3);
        } else if (lux >= 50 && lux <= 100) {
            parameters.setExposureCompensation(2);
        } else if (lux > 100 && lux < 200) {
            parameters.setExposureCompensation(1);
        } else if (lux >= 200 && lux <= 400) {
            parameters.setExposureCompensation(0);
        } else if (lux > 400) {
            parameters.setExposureCompensation(-1);
        } else if (lux > 500) {
            parameters.setExposureCompensation(-2);
        }
        camera.setParameters( parameters);
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
    }
};

镜头自动缩放

zxing原始库没有提供在二维码无法识别的情况下,进行自动放大识别,在图片距离镜头较远的情况下,图像太小无法识别。

zxing改造

让扫描器在每个缩放度(zoom)上检测10遍,如果10遍之后,仍然不能成功识别二维码,调节camera的zoom值来放大图像。

为什么是十遍,不是一遍?

因为再清晰的二维码,受到各种因素的影响(比如扫描的时候手抖了,可能导致没有捕捉到足够清晰的画面),因此扫码库一次识别成功的概率很低,所以要在每个缩放度上进行多次识别。

十次识别后如果识别不了,基本上在这个 缩放度 下就无法识别二维码了,并且十次扫描,不会让相机过快的放大(比如只设置每个zoom值下识别两次,在没有识别成功的情况下镜头会放大的很快,体验极差)。

根据这个思路来改造Zxing库

从QRCode的识别类(QRCodeReader)开始修改主干逻辑:用一个int值scanNum控制每个放大程度上的识别次数,每当大于10次的时候,才进入放大逻辑。

QRCodeReader.java 

@Override
public final Result decode(BinaryBitmap image, Map<DecodeHintType, ?> hints)
    throws NotFoundException, ChecksumException, FormatException {

  DecoderResult decoderResult;
  ResultPoint[] points;
  if (hints != null && hints.containsKey(DecodeHintType.PURE_BARCODE)) {
    BitMatrix bits = extractPureBits(image.getBlackMatrix());
    decoder.setActivity(activity);
    decoderResult = decoder.decode(bits, hints);
    points = NO_POINTS;
  } else {
    if (scanNum <= 10) {
      scanNum++;
    }
    //1、将图像进行二值化处理,1、0代表黑、白。( 二维码的使用getBlackMatrix方法 )
    //2、寻找定位符、校正符,然后将原图像中符号码部分取出。(detector代码实现的功能)
    DetectorResult detectorResult 
        = new Detector(image.getBlackMatrix(), activity).detect(hints);

//      if (detectorResult==null) {
//        Log.i("detect","检测失败");
//      } else {
//        Log.i("detect","检测成功");
//      }

    if (scanNum > 10) {  // 控制每个放大程度上的检测次数
      doCameraZoom(detectorResult, image.getBlackMatrix());
      scanNum = 0;
    }

    //3、对符号码矩阵按照编码规范进行解码,得到实际信息(decoder代码实现的功能)
    decoderResult = decoder.decode(detectorResult.getBits(), hints);
    points = detectorResult.getPoints();
  }

镜头放大逻辑

放大的时候,我们应该计算二维码的边长和实际扫码框的比值,根据这个比值来设置相机对应的zoom缩放度。根据这个比值,我们能够保证放大之后的二维码不会超出扫码框,这里我们定义这个阈值为0.8,也就是二维码不能大于扫码框的0.8。

另外注意一点,计算二维码边长的时候要乘上相机的zoom值,但是zoom值面积的缩放度,用变长乘的时候需要开根号。

看代码

// 镜头放大
private void doCameraZoom(DetectorResult detectorResult, BitMatrix bitMatrix) {
  if (activity != null && activity.isAutoEnlarged()) {   //删除&&后面
    CameraManager cameraManager = activity.getCameraManager();
    ResultPoint[] p = detectorResult.getPoints();
    // 计算扫描框中的二维码的宽度,两点间距离公式
    double len12 = calLen(p[0], p[1]);
    double len13 = calLen(p[0], p[2]);
    double len23 = calLen(p[1], p[2]);
    double len = Math.max(len12, len13);
    // 根据三个定位点 计算二维码的边长
    len = Math.max(len, len23);

    Rect frameRect = cameraManager.getFramingRect();
    if (frameRect != null) {
      int frameWidth = frameRect.right - frameRect.left;
      int frameHeight = frameRect.bottom - frameRect.top;
      // 计算扫码框的边长
      double frameCross = 
          Math.sqrt(frameWidth * frameWidth + frameHeight * frameHeight);

      Camera camera = cameraManager.getCamera();
      Camera.Parameters parameters = camera.getParameters();
      // 获取相机当前放大倍数
      int zoom = parameters.getZoom();

      if (parameters.isZoomSupported()) {
        double relate; // 计算 二维码 和 扫码框 的 边长比
        if (zoom == 0) {
          zoom++;
          relate = len / frameCross;
        } else {
          // zoom是面积缩放度,求边长需要开根号
          relate = len * Math.sqrt(zoom) / frameCross;  
        }

        // 二维码放大后 边长 不能超过扫码框边长的0.8
        if (relate < 0.8) {
          if (relate < 0.3) {
            // 二维码在扫码框的占比比较低的时候, zoom稍微加大一点,但是不能过大
            // 否则 一次zoom加过大 加上 部分手机像素低,会直接导致图片失真无法识别
            zoom += 4;
          } else if (relate < 0.45) {
            zoom += 3;
          } else {
            // 快到0.8阈值的时候,zoom值需要慢慢加,让他慢慢放大
            if (len * Math.sqrt(zoom + 1) < frameCross * 0.8) {
              zoom++;
            }
          }
          parameters.setZoom(zoom);
          camera.setParameters(parameters);
        }
      }
    }
  }
}

private double calLen(ResultPoint point1, ResultPoint point2) {
  return Math.sqrt(Math.pow(point1.getX() - point2.getX(), 2) + Math.pow(point1.getY() - point2.getY(), 2));
}
DetectorResult detectorResult 
        = new Detector(image.getBlackMatrix(), activity).detect(hints);

在上一段代码的第10行,是用于探测定位符的detect方法,在无法识别到定位符的情况下(比如二维码距离手机非常远),这个时候这个方法内部会直接抛出NotFoundException,导致下面的放大逻辑走不进来。

所以在这种无法探测到定位符 且(二维码距离镜头比较远) 的场景下,要在抛异常之前也加一下二维码放大的逻辑。因为这种情况下无法探测到二维码的定位符,所以这个时候无法得到二维码的边长,也就不能通过和扫码框的比值去设置zoom了,这个时候我们应该让放大倍数尽量小,避免图片失真。

看代码

com.google.zxing.qrcode.detector.Detector.java

case 3:
  dimension++;
  NumCount.dimensionNum++;
  if (NumCount.dimensionNum == 10000) {
    NumCount.dimensionNum = 0;
  }

  // 三次为间隔执行一次,相当于,连着三次都是别不了定位符,并且判断出来二维码比较远
  // 这个时候才去执行zoom放大 
  //(相当于容错处理,如果三次都无法识别定位符,那么认为二维码基本上是距离过远)
  if (isBlackAreaSmall() && NumCount.dimensionNum % 3 == 0) {
    doCameraZoom();
  }
  throw NotFoundException.getNotFoundInstance();

// 判断扫码框中的暗色区域是否占比比较小,占比小的时候 我们认为二维码距离镜头比较远,
// 这个时候可以加zoom 
private boolean isBlackAreaSmall() {
  CameraManager cameraManager = activity.getCameraManager();
  Rect frameRect = cameraManager.getFramingRect();
  double totalArea = frameRect.width() * frameRect.height();
  int blackArea = 0;
  int maxI = image.getHeight();
  int maxJ = image.getWidth();
  int iSkip = (3 * maxI) / (4 * 57);  //调小优化?
  for (int i = iSkip - 1; i < maxI; i++) {
    for (int j = 0; j < maxJ; j++) { //改j++ 不一定以1为间隔
      if (image.get(j, i)) {
        blackArea++;
      }
    }
  }

  if (1.0 * blackArea / totalArea < 0.25) {
    return true;
  } else {
    NumCount.blackLargeNum++;
  }
  return false;
}

// 由于不能识别到定位符,zoom放大没有参照,这里放大倍数按一次 +2 处理,慢慢放大
// 不能一次放太大,放太大容易失真
private void doCameraZoom() {
  if (activity != null && activity.isAutoEnlarged()) {   //删除&&后面
    CameraManager cameraManager = activity.getCameraManager();
    Camera camera = cameraManager.getCamera();
    Camera.Parameters parameters = camera.getParameters();
    int zoom = parameters.getZoom();
    zoom += 2;
    parameters.setZoom(zoom);
    camera.setParameters(parameters);
  }
}

从算法角度


定位符缺失识别

首先普及个知识,二维码定位符识别的核心原理。

从定位符中点随便画一条线,这条线从头到尾 经过的区域,永远都是黑-白-黑-白-黑,并且比值是 1 : 1 : 3 : 1 : 1。

735f1bbf7f4c691e6fe970f56f4ae634.png

Zxing原始的扫码库不支持定位符的缺失识别,因为它定义了三种扫描方式才能定下一个二维码:基于定位符重心的横向扫码,纵向扫描,以及斜线扫描;只有在这三个条件都满足的情况下,才能辨识这个区域为一块定位符,因此不能缺失识别。

然后我们来观察一下这个定位符的模型(红色叉号区域为缺失区域):

82e10b78bb9ecb3df25f19d9235984bb.png

得到这么一个结论--定位符识别条件:

  • 条件1: 只要有一条线横向穿过定位符满足 定位符比例,就可以计算得到 定位符横向中点坐标

  • 条件2: 只有有一条线纵向穿过定位符满足 定位符比例,就可以计算得到 定位符纵向中点坐标

根据这两个坐标就可以得出定位符 重心坐标,确定定位符的位置。

根据这个想法去改造Zxing库的定位符识别逻辑:

简单解释一下下面一连串的if-else逻辑在做啥。统计扫描到的黑白块到stateCount数组,统计完毕之后,带入handlePossibleCenter()方法接着去校验这块区域是否满足 定位符的条件;(详细改造流程见代码注释)

看代码

com.google.zxing.qrcode.detector.FinderPatternFinder.java

final FinderPatternInfo find(Map<DecodeHintType, ?> hints) throws NotFoundException {
    boolean tryHarder = hints != null && hints.containsKey(DecodeHintType.TRY_HARDER);   //一般false
    boolean pureBarcode = hints != null && hints.containsKey(DecodeHintType.PURE_BARCODE);  //一般false
    // 获取宽高
    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.
    int iSkip = (3 * maxI) / (4 * MAX_MODULES);  //调小优化?
    if (iSkip < MIN_SKIP || tryHarder) {
        iSkip = MIN_SKIP;
    }
    Log.d( SKIP ,    + iSkip);

    boolean done = false;
    int[] stateCount = new int[5];
    // 遍历二进制矩阵
    for (int i = iSkip - 1; i < maxI && !done; i += iSkip) {
        // Get a row of black/white values
        stateCount[0] = 0; // 黑
        stateCount[1] = 0; // 白
        stateCount[2] = 0; // 黑
        stateCount[3] = 0; // 白
        stateCount[4] = 0; // 黑
        int currentState = 0;
        for (int j = 0; j < maxJ; j++) { //改j++ 不一定以1为间隔
            if (image.get(j, i)) {
                // Black pixel
                if ((currentState & 1) == 1) { // Counting white pixels
                    //扫描到黑块,但是当前正在计数白块,应该将索引+1
                    currentState++;
                }
                stateCount[currentState]++;
            } else { // White pixel
                if ((currentState & 1) == 0) { // Counting black pixels
                    if (currentState == 4) { // A winner?  已经扫描到定位符的右边缘
                        if (foundPatternCross(stateCount)) { // Yes  是否为11311比例
                            // 如果横向满足比例,接着去处理这个可能为定位的位置
                            boolean confirmed = handlePossibleCenter(stateCount, i, j, pureBarcode); //第i行 第j列 检查定位符以及是否已经存在
                            if (confirmed) {
                                // Start examining every other line. Checking each line turned out to be too
                                // expensive and didn't improve performance. 开始检查每一行。 检查每一行结果太昂贵,并没有提高性能。
                                iSkip = 2;
                                if (hasSkipped) {   //
                                    done = haveMultiplyConfirmedCenters();
                                } else {
                                    int rowSkip = findRowSkip();
                                    if (rowSkip > stateCount[2]) {
                                        // Skip rows between row of lower confirmed center
                                        // and top of presumed third confirmed center
                                        // but back up a bit to get a full chance of detecting
                                        // it, entire width of center of finder pattern

                                        // Skip by rowSkip, but back off by stateCount[2] (size of last center
                                        // of pattern we saw) to be conservative, and also back off by iSkip which
                                        // is about to be re-added
                                        i += rowSkip - stateCount[2] - iSkip;
                                        j = maxJ - 1;
                                    }
                                }
                            } else {
                                stateCount[0] = stateCount[2];  //?
                                stateCount[1] = stateCount[3];
                                stateCount[2] = stateCount[4];
                                stateCount[3] = 1;
                                stateCount[4] = 0;
                                currentState = 3;
                                continue;
                            }
                            // Clear state to start looking again
                            currentState = 0;
                            stateCount[0] = 0;
                            stateCount[1] = 0;
                            stateCount[2] = 0;
                            stateCount[3] = 0;
                            stateCount[4] = 0;
                        } else { // No, shift counts back by two 班次重新计算两次
                            stateCount[0] = stateCount[2];
                            stateCount[1] = stateCount[3];
                            stateCount[2] = stateCount[4];
                            stateCount[3] = 1;
                            stateCount[4] = 0;
                            currentState = 3;
                        }
                    } else {  //4
                        stateCount[++currentState]++;
                    }
                } else { // Counting white pixels
                    stateCount[currentState]++;
                }
            }
        }
        if (foundPatternCross(stateCount)) {
            boolean confirmed = handlePossibleCenter(stateCount, i, maxJ, pureBarcode);
            if (confirmed) {
                iSkip = stateCount[0];
                if (hasSkipped) {
                    // Found a third one
                    done = haveMultiplyConfirmedCenters();
                }
            }
        }
    }
//进一步处理已通过定位符横向扫描的位置 (后续会进行纵向扫描)
    protected final boolean handlePossibleCenter(int[] stateCount, int i, int j, boolean pureBarcode) {
        int stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2] + stateCount[3] +
            stateCount[4]; //1+1+3+1+1=7 约等于7的倍数
        float centerJ = centerFromEnd(stateCount, j); //水平方向中心点坐标
        // 横向扫描只能求出 重心的x轴坐标,需要进行纵向扫描求出y轴坐标
        float centerI = crossCheckVertical(i, (int) centerJ, stateCount[2], stateCountTotal);  //求垂直方向中心坐标 改
        if (!Float.isNaN(centerI)) {
            // Re-cross check 重新检验
//      centerJ = crossCheckHorizontal((int) centerJ, (int) centerI, stateCount[2], stateCountTotal);  //错误,因为原先(centerJ,centerI)指向白区,可以删除,也可以需要修改centerJ,让远点指向黑区
            if (!Float.isNaN(centerJ) &&
                (!pureBarcode)) {  
                //原判断条件为(!pureBarcode || crossCheckDiagonal(centerJ, cenerI)),
                //crossCheckDiagonal()为对角线检验, 按照上述实现思路需要删掉 
                //pureBarcode一般为false
                float estimatedModuleSize = stateCountTotal / 7.0f;  //一个小黑块的边长
                boolean found = false;
                for (int index = 0; index < possibleCenters.size(); index++) {
                    FinderPattern center = possibleCenters.get(index);
                    // Look for about the same center and module size:
                    if (center.aboutEquals(estimatedModuleSize, centerI, centerJ)) {
                        possibleCenters.set(index, center.combineEstimate(centerI, centerJ, estimatedModuleSize));
                        found = true;
                        break;
                    }
                }
                if (!found) {
                    FinderPattern point = new FinderPattern(centerJ, centerI, estimatedModuleSize);
                    possibleCenters.add(point);
                    if (resultPointCallback != null) {
                        resultPointCallback.foundPossibleResultPoint(point);
                    }
                }
                return true;
            }
        }
        return false;
    }

纵向扫描改造

原先的纵向扫描是从下图绿线的中点位置,开始扫描。显然这种扫描方式,一旦纵向中线缺失会导致定位符无法识别;

改造后

外重循环从左侧黄线的x轴到右侧黄线,其中只要存在一条黄线满足11311定位符比例,则认为这块区域是一个可能的定位符。

6430c5a6ee5acf30becbf7f8f0cb2945.png

private float crossCheckVertical(int startI, int centerJ, int maxCount,
                                 int originalStateCountTotal) {
    BitMatrix image = this.image;
    int maxI = image.getHeight();
    int[] stateCount = getCrossCheckStateCount();

    int minJ = centerJ, maxJ = centerJ; //中间黑正方形水平方向的边界值

    while (image.get(maxJ, startI)) {
        maxJ++;
    }
    maxJ--;
    while (image.get(minJ, startI)) {
        minJ--;
    }
    minJ++;
    Log.d( J , minJ +     + maxJ);

    loop:
    for (int k = minJ; k <= maxJ; k++) {
        // Start counting up from center
        int i = startI;  //垂直方向
        while (i >= 0 && image.get(k, i)) { //startI上边到白块以前的黑块数量  改?
            stateCount[2]++;
            i--;
        }
        if (i < 0) {  //改?
            resetStateCount(stateCount);
            continue loop;
        }
        while (i >= 0 && !image.get(k, i) && stateCount[1] <= maxCount) { //统计白块数量
            stateCount[1]++;
            i--;
        }
        // If already too many modules in this state or ran off the edge:
        // Log.d( 1 and maxCount ,stateCount[1]+   +maxCount);
        if (i < 0 || stateCount[1] > maxCount) {
            resetStateCount(stateCount);
            continue loop;
        }
        while (i >= 0 && image.get(k, i) && stateCount[0] <= maxCount) { //统计外边框的黑块数量
            stateCount[0]++;
            i--;
        }
        if (stateCount[0] > maxCount) {
            resetStateCount(stateCount);
            continue loop;
        }

        // Now also count down from center
        i = startI + 1;
        while (i < maxI && image.get(k, i)) {
            stateCount[2]++;
            i++;
        }
        if (i == maxI) {
            resetStateCount(stateCount);
            continue loop;
        }
        while (i < maxI && !image.get(k, i) && stateCount[3] < maxCount) {
            stateCount[3]++;
            i++;
        }
        if (i == maxI || stateCount[3] >= maxCount) {
            resetStateCount(stateCount);
            continue loop;
        }
        while (i < maxI && image.get(k, i) && stateCount[4] < maxCount) {
            stateCount[4]++;
            i++;
        }
        if (stateCount[4] >= maxCount) {   //?
            resetStateCount(stateCount);
            continue loop;
        }

        // If we found a finder-pattern-like section, but its size is more than 40% different than
        // the original, assume it's a false positive
        int stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2] + stateCount[3] +
            stateCount[4];
        if (5 * Math.abs(stateCountTotal - originalStateCountTotal) >= 2 * originalStateCountTotal) {  //倾斜误差范围  originalStateCountTotal横向相加 stateCountTotal纵向
            resetStateCount(stateCount);
            continue loop;
        }
        //检测是否满足11311比例
        if (foundPatternCross(stateCount)) {
            return centerFromEnd(stateCount, i);   //是否得有一定宽度,即满足11311比例的数量,先假设否
        } else {
            resetStateCount(stateCount);
            continue loop;
        }
    }
    return Float.NaN;
}

注意:上面提到的删除定位符斜向扫描的修改,还有待考究,因为虽然这样增加的定位符识别的可缺失面积,但是这相当于反向减少了二维码的辨识度;不过笔者实际测试的时候没有发现问题,保险起见的话,应该保留斜向扫描的逻辑。

减少解码格式

Zxing扫码库默认支持扫描15种格式(包括一维码和二维码),但实际应用中无需支持这么多码型,把不支持的剔除掉可以增加扫码速度。一般的扫码器仅支持QRCode就够了,所以其他无用的码型建议全部干掉。

public BitmapDecoder(Context context) {

   multiFormatReader = new MultiFormatReader();

   // 解码的参数
   Hashtable<DecodeHintType, Object> hints = new Hashtable<DecodeHintType, Object>(
         2);
   // 可以解析的编码类型
   Vector<BarcodeFormat> decodeFormats = new Vector<BarcodeFormat>();
   if (decodeFormats == null || decodeFormats.isEmpty()) {
      decodeFormats = new Vector<BarcodeFormat>();

      // 这里设置可扫描的类型,我这里选择了都支持
      // 这里对具体码型分了三大类,具体想要选择哪几种请进入数组内部查看
      decodeFormats.addAll(DecodeFormatManager.ONE_D_FORMATS);
      decodeFormats.addAll(DecodeFormatManager.QR_CODE_FORMATS);
      decodeFormats.addAll(DecodeFormatManager.DATA_MATRIX_FORMATS);
   }
   hints.put(DecodeHintType.POSSIBLE_FORMATS, decodeFormats);

   // 设置继续的字符编码格式为UTF8
   hints.put(DecodeHintType.CHARACTER_SET,  UTF8 );

   // 设置解析配置参数
   multiFormatReader.setHints(hints);

}

/   生成篇   /

背景

我们是否这样的遇到过这样的场景,在产品台有n个产品包,服务端对每个产品包都下发了一个url,这个时候我们把这些url一一编成二维码。 但是!!!因为下发的url长短不一,发现生成的二维码会有不同宽度的白边,导致用户视觉体验差。

原因

好了,先来分析为什么会这样。在生成二维码的时候,我们传入了需要生成的图像的宽高。这个时候整个图像的宽高是确定的,但是你只确定了整个图像的宽高啊,没有确定二维码的宽高呀。

实际上二维码的宽高是不确定的,为毛这么说? 来看一张图。

b26bc89f77a78de99511a946a8d21005.png

二维码总共有40个版本,每个版本对应的尺寸都不一样版本1边长为21位二进制 , 往后每增加一个版本,边长增加4位二进制。

所以你传入的url的长短不一样,扫码库会根据你传入字符串的长度选择合适的版本,然后生成不同宽高的二维码,所以我们就知道了为什么 得到的二维码图像 总会有不同的白边。

那么这个能解决吗?当然是可以的。

源码

先来看一下zxing的代码库。

ee36cc091f2dce7e6a27323a5b113093.png

看到这个仓里有好多包,每个包对应一种码型,我们要找的就是这个qrcode二维矩阵码包。找到这个包下的QRCodeWriter编码类,先看他的编码方法encode()。

public BitMatrix encode(String contents,
                        BarcodeFormat format,
                        int width,
                        int height,
                        Map<EncodeHintType,?> hints) throws WriterException {

  if (contents.isEmpty()) {
    throw new IllegalArgumentException("Found empty contents");
  }

  if (format != BarcodeFormat.QR_CODE) {
    throw new IllegalArgumentException("Can only encode QR_CODE, but got " + format);
  }

  if (width < 0 || height < 0) {
    throw new IllegalArgumentException("Requested dimensions are too small: " + width + 'x' +
        height);
  }

  ErrorCorrectionLevel errorCorrectionLevel = ErrorCorrectionLevel.L;
  int quietZone = QUIET_ZONE_SIZE;
  if (hints != null) {
    if (hints.containsKey(EncodeHintType.ERROR_CORRECTION)) {
      errorCorrectionLevel = ErrorCorrectionLevel.valueOf(hints.get(EncodeHintType.ERROR_CORRECTION).toString());
    }
    if (hints.containsKey(EncodeHintType.MARGIN)) {
      quietZone = Integer.parseInt(hints.get(EncodeHintType.MARGIN).toString());
    }
  }
  // 真正开始执行编码
  QRCode code = Encoder.encode(contents, errorCorrectionLevel, hints);
  // 重点方法:将得到的QRCode转化为二进制矩阵BitMatrix,并加入白边
  return renderResult(code, width, height, quietZone);
}

下面来看这个重要的添加白边的方法 renderResult()。

// Note that the input matrix uses 0 == white, 1 == black, while the output matrix uses
// 0 == black, 255 == white (i.e. an 8 bit greyscale bitmap).
private static BitMatrix renderResult(QRCode code, int width, int height, int quietZone) {
  ByteMatrix input = code.getMatrix();
  if (input == null) {
    throw new IllegalStateException();
  }
  // 输入的宽高
  int inputWidth = input.getWidth();
  int inputHeight = input.getHeight();

      // quiteZone为初始白边的宽度
  int qrWidth = inputWidth + (quietZone * 2);
  int qrHeight = inputHeight + (quietZone * 2);

  // 和用户输入的宽高比一比,取大者作为最终输出的宽高
  int outputWidth = Math.max(width, qrWidth);
  int outputHeight = Math.max(height, qrHeight);

  // 计算缩放比
  int multiple = Math.min(outputWidth / qrWidth, outputHeight / qrHeight);
  // Padding includes both the quiet zone and the extra white pixels to accommodate the requested
  // dimensions. For example, if input is 25x25 the QR will be 33x33 including the quiet zone.
  // If the requested size is 200x160, the multiple will be 4, for a QR of 132x132. These will
  // handle all the padding from 100x100 (the actual QR) up to 200x160.
  // 计算额外需要加的白边的宽度
  int leftPadding = (outputWidth - (inputWidth * multiple)) / 2;
  int topPadding = (outputHeight - (inputHeight * multiple)) / 2;

  BitMatrix output = new BitMatrix(outputWidth, outputHeight);

  // 编码ByteMatrix矩阵,将ByteMatrix的内容计算padding后转换成二进制矩阵BitMatrix输出
  for (int inputY = 0, outputY = topPadding; inputY < inputHeight; inputY++, outputY += multiple) {
    // Write the contents of this row of the barcode
    for (int inputX = 0, outputX = leftPadding; inputX < inputWidth; inputX++, outputX += multiple) {
      if (input.get(inputX, inputY) == 1) {
        output.setRegion(outputX, outputY, multiple, multiple);
      }
    }
  }

  return output;
}

看到这里就应该已经知道如何去白边了吧。我们把这个QRCodeWriter类copy一份然后改造下里面的renderResult()方法,把里面的两个padding的地方改一下,就。。ok了

动手

方法1:重写Writer生成类

// Note that the input matrix uses 0 == white, 1 == black, while the output matrix uses
// 0 == black, 255 == white (i.e. an 8 bit greyscale bitmap).
 private static BitMatrix renderResult(QRCode code, int width, int height, int quietZone) {
  ByteMatrix input = code.getMatrix();
  if (input == null) {
    throw new IllegalStateException();
  }
  int inputWidth = input.getWidth();
  int inputHeight = input.getHeight();
  int outputWidth = Math.max(width, inputWidth);
  int outputHeight = Math.max(height, inputWidth);

  int multiple = Math.min(outputWidth / inputWidth, outputHeight / inputHeight);
  // Padding includes both the quiet zone and the extra white pixels to accommodate the requested
  // dimensions. For example, if input is 25x25 the QR will be 33x33 including the quiet zone.
  // If the requested size is 200x160, the multiple will be 4, for a QR of 132x132. These will
  // handle all the padding from 100x100 (the actual QR) up to 200x160.


  BitMatrix output = new BitMatrix(outputWidth, outputHeight);

  for (int inputY = 0, outputY = 0; inputY < inputHeight; inputY++, outputY += multiple) {
    // Write the contents of this row of the barcode
    for (int inputX = 0, outputX = 0; inputX < inputWidth; inputX++, outputX += multiple) {
      if (input.get(inputX, inputY) == 1) {
        output.setRegion(outputX, outputY, multiple, multiple);
      }
    }
  }

  return output;
}

看下改造前后的效果。

62af85b42969de0e07704b122ddd3cd4.png

测试代码

public void generateQrCodeWithoutWhiteBorder(View view) {
    QRCodeWithoutWhiteBorderWriter qrCodeWriter = new QRCodeWithoutWhiteBorderWriter();
    Map<EncodeHintType, String> hints = new HashMap<>();
    hints.put(EncodeHintType.CHARACTER_SET, "utf-8"); //记得要自定义长宽
    BitMatrix encode = null;
    try {
        encode = qrCodeWriter.encode("hello world", BarcodeFormat.QR_CODE, width, height, hints);
    } catch (WriterException e) {
        e.printStackTrace();
    }
    int[] colors = new int[width * height];
    //利用for循环将要表示的信息写出来
    for (int i = 0; i < width; i++) {
        for (int j = 0; j < height; j++) {
            if (encode.get(i, j)) {
                colors[i * width + j] = Color.BLACK;
            } else {
                colors[i * width + j] = Color.WHITE;
            }
        }
    }

    Bitmap bit = Bitmap.createBitmap(colors, width, height, Bitmap.Config.RGB_565);
    mIvQrCodeWihoutWhiteBorder.setImageBitmap(bit);
}

这个时候有些人就要说了,能不能不侵入Zxing库的代码? 也是可以的,下面介绍第二种方法。

方法2:外部二次处理法

这个时候我们需要从上面的QRCodeWriter的encode()方法返回的BitMatrix入手了。因为我们知道01矩阵在这个带白框矩阵中的位置,所以我们把里面的二维码矩阵,单独抽出来就行了。

看代码

稍微拓展了一下,这个方法是重新定义二维码白边的宽度,如果你不想要白边,margin传0就行。

private static BitMatrix updateBit(BitMatrix matrix, int margin) {
    int tempM = margin * 2;
    int[] rec = matrix.getEnclosingRectangle(); // 获取二维码图案的属性
    // 感兴趣可以进入这个getEnclosingRecting()方法看一下
    // rec[0]表示 left:二维码距离矩阵左边缘的距离
    // rec[1]表示 top:二维码距离矩阵上边缘的距离
    // rect[2]表示二维码的宽
    // rect[2]表示二维码的高
    int resWidth = rec[2] + tempM;
    int resHeight = rec[3] + tempM;
    BitMatrix resMatrix = new BitMatrix(resWidth, resHeight); // 按照自定义边框生成新的BitMatrix
    resMatrix.clear();
    for (int i = margin; i < resWidth - margin; i++) { // 循环,将二维码图案绘制到新的bitMatrix中
        for (int j = margin; j < resHeight - margin; j++) {
            if (matrix.get(i - margin + rec[0], j - margin + rec[1])) {
                resMatrix.set(i, j);
            }
        }
    }
    return resMatrix;
}

测试代码

public void generateCommonQrCode(View view) {
    QRCodeWriter qrCodeWriter = new QRCodeWriter();
    Map<EncodeHintType, String> hints = new HashMap<>();
    hints.put(EncodeHintType.CHARACTER_SET, "utf-8"); //记得要自定义长宽
    BitMatrix encode = null;
    try {
        encode = qrCodeWriter.encode("hello world", BarcodeFormat.QR_CODE, width, height, hints);
    } catch (WriterException e) {
        e.printStackTrace();
    }
    encode = updateBit(encode, 0);
    int newWidth = encode.getWidth();
    int newHeight = encode.getHeight();
    int[] colors = new int[newWidth * newHeight];
    //利用for循环将要表示的信息写出来
    for (int i = 0; i < newWidth; i++) {
        for (int j = 0; j < newHeight; j++) {
            if (encode.get(i, j)) {
                colors[i * newWidth + j] = Color.BLACK;
            } else {
                colors[i * newWidth + j] = Color.WHITE;
            }
        }
    }
    Bitmap bit = Bitmap.createBitmap(colors, newWidth, newWidth, Bitmap.Config.RGB_565);
    mCommonQrCode.setImageBitmap(bit);
}

QR-Code编码原理:

https://juejin.cn/post/7071499529995943950

Zxing如何生成无白边的二维码:

https://juejin.cn/post/7066823758807302151

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

Android终于要推出Google官方的二维码扫描库了?

仿微信做个极速二维码扫描功能

欢迎关注我的公众号

学习技术或投稿

c46493212c04c4dca6977bfd664f60e7.png

ae346ea6c205f02ac64dc9dc7a3e791f.png

长按上图,识别图中二维码即可关注

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值