概述
/ 今日科技快讯 /
近日,抖音直播发布《抖音关于打击直播诈骗黑色产业链的公告》。公告称,为进一步保障用户及平台安全,抖音下阶段将重点推进MCN治理、帐号治理等专项行动。同时,将于即日起开展针对直播黑产的专项打击。对于违规情节严重或涉及违法犯罪行为的主播及MCN/公会,平台会主动上报行业黑名单并将相关情况报送公安机关等有关部门。
/ 作者简介 /
本篇文章来自红鲤鱼鲤驴与驴的投稿,文章主要分享了他对Zxing扫码库优化的思路,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。
红鲤鱼鲤驴与驴的博客地址:
https://juejin.cn/user/2960298998247624
/ Zxing库结构 /
优化之前先来简单介绍一下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。
Zxing原始的扫码库不支持定位符的缺失识别,因为它定义了三种扫描方式才能定下一个二维码:基于定位符重心的横向扫码,纵向扫描,以及斜线扫描;只有在这三个条件都满足的情况下,才能辨识这个区域为一块定位符,因此不能缺失识别。
然后我们来观察一下这个定位符的模型(红色叉号区域为缺失区域):
得到这么一个结论--定位符识别条件:
条件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定位符比例,则认为这块区域是一个可能的定位符。
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长短不一,发现生成的二维码会有不同宽度的白边,导致用户视觉体验差。
原因
好了,先来分析为什么会这样。在生成二维码的时候,我们传入了需要生成的图像的宽高。这个时候整个图像的宽高是确定的,但是你只确定了整个图像的宽高啊,没有确定二维码的宽高呀。
实际上二维码的宽高是不确定的,为毛这么说? 来看一张图。
二维码总共有40个版本,每个版本对应的尺寸都不一样版本1边长为21位二进制 , 往后每增加一个版本,边长增加4位二进制。
所以你传入的url的长短不一样,扫码库会根据你传入字符串的长度选择合适的版本,然后生成不同宽高的二维码,所以我们就知道了为什么 得到的二维码图像 总会有不同的白边。
那么这个能解决吗?当然是可以的。
源码
先来看一下zxing的代码库。
看到这个仓里有好多包,每个包对应一种码型,我们要找的就是这个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;
}
看下改造前后的效果。
测试代码
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官方的二维码扫描库了?
仿微信做个极速二维码扫描功能
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注
最后
以上就是舒服小松鼠为你收集整理的Zxing扫码库优化思路扫描流程的全部内容,希望文章能够帮你解决Zxing扫码库优化思路扫描流程所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复