2021SC@SDUSC
前面的博客分析了Zxing解码流程及对一维码、二维码、混合编码不同码制的解码算法。下面介绍一些辅助的类和测试代码。
一、common包
- detector文件夹中的类有两个类MathUtils和WhiteRectangleDetector。
- MathUtils中有四舍五入、计算两点之间的欧氏距离和数组求和这三个算法。
- WhiteRectangleDetector的作用是检测图像中类似条形码的矩形区域。它从图像中心开始,增大候选区域的大小,直到找到白色矩形区域。通过跟踪最后遇到的黑点,它可以确定条形码的角点。 - reedsolomon文件夹中是有关galois field和Reed-Solomon算法的一些类,这部分在我和队友之前的博客中都提到过了,需要注意的是,在aztec、datamatrix、maxicode、qrcode的Decode中都有用到这部分算法,在aztec的Detector中也用到了这部分算法。
- BitArray:是一个位数组,内部用整数数组表示。在条形码中较常见——因为条形码只有一个维度存放了信息,可以用一位数组存储
- BitMatrix:表示位的二维矩阵,与BitArray相对应,在二维码中较为常见。
- BitSource:是从字节序列中读取位的方法。在QRcode中有的编码方式可能指定每10位编码三个数字,因此需要从字节序列读取位;这个类还有一个用途是判断调用者是否试图读取比二维码可用的位更多的位,如果是,则抛出异常。
- CharacterSetECI:是一个枚举类,根据ISO 18004的第1.1条规定,封装ECI字符集
- DecoderResult:通常在二维码中使用,用于保存解码的中间结果。
- 该类的实现可以在给定图像中二维码的查找器模式位置的情况下,对图像中的正确点进行采样以重建二维码,从而考虑透视失真。
- DefaultGridSampler:对tGridSampler的继承,override了一些方法。
- DetectorResult:封装在图像中检测码的结果。一般用于二维码。 包括了对应于码的黑/白像素的原始矩阵,以及图像中可能的定位点等。
- GlobalHistogramBinarizer:继承了Binarizer,实现旧的ZXing全局直方图方法。它适用于没有足够CPU或内存使用局部阈值算法的低端移动设备。但是,由于它拾取了全局黑点,因此无法处理困难的阴影和渐变。
- HybridBinarizer:这个类实现了一个局部阈值算法,虽然比globalHistorogrambinarizer慢,但它的效率相当高。它是为白色背景上带有黑色数据的条形码的高频图像而设计的。该类扩展了GlobalHistorogrambinarizer,对1D读取器使用较旧的直方图方法,对2D读取器使用较新的局部方法。
- PerspectiveTransform:此类在二维码中实现透视变换。前面的博客中已经介绍过这个算法
- StringUtils:常见的字符串相关函数。
大部分的类在之间的博客中已经提到了,下面根据aztec介绍一下还没有提到的算法和类。
二、aztec
aztec中有关解码的部分如下所示:
- AztecReader:该实现可以检测和解码图像中的Aztec代码。
- Decoder:实现Aztec Code解码的主要类——与从图像中定位和提取Aztec Code相对。
- Detector:封装可以在图像中检测Aztec代码的逻辑,即使Aztec代码旋转、扭曲或部分模糊。
- AztecDetectorResult:使用Aztec格式特有的更多信息扩展DetectorResult,如层数和是否紧凑。
一、aztec介绍
它独特的位于正中的模式识别标志和安置算法使Aztec看起来像个旋涡一样。通过图像我们可以知道,aztec与QRcode最大的区别在于定位算法。因此这部分我们主要介绍aztec的Detector。
aztec主要是螺旋式编码,构筑在方形网格上,其中心有一个“牛眼”图案用以定位该码,数据围绕该牛眼图案做同心方形环状编码。中心的“牛眼”为99或1313像素,并在周围的一行像素编码基本编码参数,产生一个11 * 11或15 * 15的核心。而数据以层,每个层包含2环像素,总像素形成15 * 15、19 * 19、23 * 23等。
核心的边角存在方向标记,以支持图案被旋转或镜像时读取代码。解码从有三个像素的边角开始,然后顺时针到两个像素、一个像素、零个像素的边角。在中心的核心编码载有尺寸信息,所以不需要其他一些条码所需要的空白“静区”来标记代码边缘。
紧凑的Aztec代码的核心(红色斜线阴影线),显示了中央靶心,四个方向标记(蓝色斜线阴影线)和28位(每侧7位)编码信息(绿色水平线)的空间孵化)。数据的第一圈从该圈开始(灰色斜线阴影线)。
完整的Aztec代码的核心。方向标记之间有40位可用于编码参数。
消息数据围绕核心呈螺旋状放置。模式消息以“ 01011100”开始,表示01(2进制) +1 = 2层,以及011100(2进制) +1 = 29个数据码字(每个6位)。
编码过程含一下步骤:
1.将源消息转换为字符串比特
2.计算必要的符号大小和模式消息,用以决定Reed-Solomon码字大小
3.对消息比特补足为Reed-Solomon码字
4.消息填充到码字边界
5.追加检查码字
6.围绕核心以螺旋形式排列完整信息
而解码可简单理解为这个过程的逆过程
所有8位的值都可编码,另外加上两个转义代码。默认情况下,0-127的码遵循ANSI*3.4(ASCII)解释,128-255遵循ISO 8859-1:Latin AIphabet No.1解释,这对应ECI 000003。
模式消息在消息中编码了层数(层数L 编码为整数L−1)和数据码字(codewords)数量(码字D编码为整数D−1)。剩余的码字用作检查码字。
压缩 Aztec Code 条形码可以具有 1 到 4 个数据层,而全范围 Aztec Code 条形码的范围是从 1 到 32 个数据层。这个数据层是Aztec Code 条形码特有的符号体系特殊选项。以下图像显示含各自数据层的 Aztec Code 符号:
二、解码流程
根据上述aztec的特点,我们可以总结出aztec的解码流程:得到“牛眼”图案的中心(获取定位点)->根据编码特点,从定位点周围得到模式信息和数据信息->螺旋式解码
三、定位算法
//封装检测Aztec代码的结果,如果参数为true,则图像是原始图像的镜像
public AztecDetectorResult detect(boolean isMirror) throws NotFoundException {
// 得到aztec矩阵的中心
Point pCenter = getMatrixCenter();
//将四个对角点的中心点正好放在靶心之外
// [topRight, bottomRight, bottomLeft, topLeft]
ResultPoint[] bullsEyeCorners = getBullsEyeCorners(pCenter);
if (isMirror) {
ResultPoint temp = bullsEyeCorners[0];
bullsEyeCorners[0] = bullsEyeCorners[2];
bullsEyeCorners[2] = temp;
}
// 从靶心处获取矩阵的大小和其他参数
extractParameters(bullsEyeCorners);
// 对网格进行采样
BitMatrix bits = sampleGrid(image,
bullsEyeCorners[shift % 4],
bullsEyeCorners[(shift + 1) % 4],
bullsEyeCorners[(shift + 2) % 4],
bullsEyeCorners[(shift + 3) % 4]);
// 获取矩阵的角点
ResultPoint[] corners = getMatrixCornerPoints(bullsEyeCorners);
return new AztecDetectorResult(bits, corners, compact, nbDataBlocks, nbLayers);
}
其中主要的算法就是获取中心位置,该算法如下:
private Point getMatrixCenter() {
ResultPoint pointA;
ResultPoint pointB;
ResultPoint pointC;
ResultPoint pointD;
//获取一个白色矩形,该矩形可以是中心靶心中矩阵的边界
try {
//WhiteRectangleDetector的介绍见本篇文章common部分
ResultPoint[] cornerPoints = new WhiteRectangleDetector(image).detect();
pointA = cornerPoints[0];
pointB = cornerPoints[1];
pointC = cornerPoints[2];
pointD = cornerPoints[3];
} catch (NotFoundException e) {
// 如果初始矩形为白色,则可能出现此异常
// 在这种情况下,毫无疑问,在靶心中,我们尝试扩展矩形。
int cx = image.getWidth() / 2;
int cy = image.getHeight() / 2;
//获取给定方向上具有不同颜色的第一个点的坐标
pointA = getFirstDifferent(new Point(cx + 7, cy - 7), false, 1, -1).toResultPoint();
pointB = getFirstDifferent(new Point(cx + 7, cy + 7), false, 1, 1).toResultPoint();
pointC = getFirstDifferent(new Point(cx - 7, cy + 7), false, -1, 1).toResultPoint();
pointD = getFirstDifferent(new Point(cx - 7, cy - 7), false, -1, -1).toResultPoint();
}
//计算矩形的中心,这里用到的是MathUtils四舍五入的算法
int cx = MathUtils.round((pointA.getX() + pointD.getX() + pointB.getX() + pointC.getX()) / 4.0f);
int cy = MathUtils.round((pointA.getY() + pointD.getY() + pointB.getY() + pointC.getY()) / 4.0f);
// 从先前计算的中心重新确定白色矩形
// 这将确保我们最终在中心靶心处有一个白色矩形,以便计算出更精确的中心。
try {
ResultPoint[] cornerPoints = new WhiteRectangleDetector(image, 15, cx, cy).detect();
pointA = cornerPoints[0];
pointB = cornerPoints[1];
pointC = cornerPoints[2];
pointD = cornerPoints[3];
} catch (NotFoundException e) {
// 如果初始矩形为白色,则可能出现此异常
// 在这种情况下,我们尝试展开矩形。
pointA = getFirstDifferent(new Point(cx + 7, cy - 7), false, 1, -1).toResultPoint();
pointB = getFirstDifferent(new Point(cx + 7, cy + 7), false, 1, 1).toResultPoint();
pointC = getFirstDifferent(new Point(cx - 7, cy + 7), false, -1, 1).toResultPoint();
pointD = getFirstDifferent(new Point(cx - 7, cy - 7), false, -1, -1).toResultPoint();
}
//重新计算矩形的中心
cx = MathUtils.round((pointA.getX() + pointD.getX() + pointB.getX() + pointC.getX()) / 4.0f);
cy = MathUtils.round((pointA.getY() + pointD.getY() + pointB.getY() + pointC.getY()) / 4.0f);
return new Point(cx, cy);
}
四舍五入算法:
public static int round(float d) {
return (int) (d + (d < 0.0f ? -0.5f : 0.5f));
}
获取给定方向上具有不同颜色的第一个点的坐标
private Point getFirstDifferent(Point init, boolean color, int dx, int dy) {
int x = init.getX() + dx;
int y = init.getY() + dy;
while (isValid(x, y) && image.get(x, y) == color) {
x += dx;
y += dy;
}
x -= dx;
y -= dy;
while (isValid(x, y) && image.get(x, y) == color) {
x += dx;
}
x -= dx;
while (isValid(x, y) && image.get(x, y) == color) {
y += dy;
}
y -= dy;
return new Point(x, y);
}
四、纠错
在common中我们提到,aztec的Detector中用到了ReedSolomonDecoder,这是为了使用Reed-Solomon算法校正参数位。
//从靶心周围的层中提取数据层和数据块的数量。
private void extractParameters(ResultPoint[] bullsEyeCorners) throws NotFoundException {
if (!isValid(bullsEyeCorners[0]) || !isValid(bullsEyeCorners[1]) ||
!isValid(bullsEyeCorners[2]) || !isValid(bullsEyeCorners[3])) {
throw NotFoundException.getNotFoundInstance();
}
int length = 2 * nbCenterLayers;
// 从靶心上取部分数据
int[] sides = {
sampleLine(bullsEyeCorners[0], bullsEyeCorners[1], length), // Right side右侧
sampleLine(bullsEyeCorners[1], bullsEyeCorners[2], length), // Bottom底部
sampleLine(bullsEyeCorners[2], bullsEyeCorners[3], length), // Left side左侧
sampleLine(bullsEyeCorners[3], bullsEyeCorners[0], length) // Top顶部
};
// bullsEyeCorners[shift] 是牛眼,有三个方向标记,sides[shift]是从具有三个方向标记的角点到具有两个方向标记的角点的行/列。
shift = getRotation(sides, length);
// 将参数位展平为单个28或40位长度
long parameterData = 0;
for (int i = 0; i < 4; i++) {
int side = sides[(shift + i) % 4];
if (compact) {
// 表格的每一面..XXXXXXX.其中Xs是参数数据
parameterData <<= 7;
parameterData += (side >> 1) & 0x7F;
} else {
// 表格的每一面..XXXXX.XXXXX.其中Xs是参数数据
parameterData <<= 10;
parameterData += ((side >> 2) & (0x1f << 5)) + ((side >> 1) & 0x1F);
}
}
// 使用RS更正参数数据。仅返回数据部分而不进行错误更正。传入的前一个参数是参数位,后一个参数表示如果这是压缩的Aztec代码,则为true
int correctedData = getCorrectedParameterData(parameterData, compact);
if (compact) {
// 8位:2位层和6位数据块
nbLayers = (correctedData >> 6) + 1;
nbDataBlocks = (correctedData & 0x3F) + 1;
} else {
// 16位:5位层和11位数据块
nbLayers = (correctedData >> 11) + 1;
nbDataBlocks = (correctedData & 0x7FF) + 1;
}
}
欢迎提出宝贵意见,感谢观看!
参考: ZxingAPI