2021SC@SDUSC-Zxing(十二):二维码的解析(Decode)及纠错有关算法介绍

2021SC@SDUSC

通过上一篇文章我们可以定位到二维码,下面就来介绍如何将二维码中存放的0、1数据转换为我们需要的文本信息。

一、Decode目录分析

  • Decoder:实现二维码解码的主要类,最终得到的就是二维码中编码的文本和字节。
  • DecodedBitStreamParser:解码比特流解析器。QR码可以在多种模式中的一种模式中将文本编码为位,并且可以在一个QR码中使用多种模式。此类将位解码回文本
  • BitMatrixParser:位矩阵分析器。
  • QRCodeDecoderMetaData:用于QR码解码的元数据容器。此类的实例可用于将信息传回解码调用者。
  • DataBlock:将数据块封装在二维码中。QR码可以将其数据分成多个块,每个块都是数据和纠错码字的单位。每个类都由此类的一个实例表示。
  • Mode:这是一个枚举类。该枚举封装了各种模式,在这些模式中,数据可以编码为QR码标准中的位。根据ISO 18004:2006,6.4.1,表2和表3编写。
  • DataMask:这是一个枚举类。根据ISO 18004:2006 6.8编写,将数据位的数据掩码封装在二维码中。此类的实现可以取消对原始BitMatrix(位矩阵)的掩码。为简单起见,它们将解除整个BitMatrix的掩码,包括用于查找器模式、计时模式等的区域。这些区域在解除掩码后应不使用。
  • Version:从二维码维度推断二维码的版本信息,这一部分根据ISO 18004:2006 Annex D编写。Version中还包括两个类:ECBlocks和ECB用来封装纠错码有关的信息。
  • FormatInformation:封装二维码的格式信息,包括使用的数据掩码和纠错级别。
  • ErrorCorrectionLevel:枚举类。此枚举封装了QR码标准定义的四个错误更正级别。根据ISO 18004:2006,6.5.1编写。
    在这里插入图片描述
    在这里插入图片描述

二、解析流程

在这里插入图片描述

这一部分的decode方法return的就是我们最终需要的二维码中编码的文本和字节了。可能会抛出的异常:FormatException二维码无法解码;ChecksumException错误更正失败
涉及到的三个decode方法
对于这个过程可以这样理解:调用者相当于老板,他把项目(解码图片)交给了Decode工作小组,Decode中有三个主力成员,decode1、decode2、decode3。decode1表达能力好,他在小组中的角色是负责上传下达,并最终给老板进行工作汇报(解码结果);decode2理解能力好,他把decode1传达的消息进行整合(传递信息);decode3技术能力好,他主要负责攻克工作难点(对字节流进行解码)。
QRCodeReader中调用的docode方法:DecoderResult decode(BitMatrix bits, Map<DecodeHintType,?> hints)。这个方法是调用者看得见摸得着、实际调用的的decode方法,主要负责去除掩码、镜像读取版本和格式信息、反镜像、并把解码结果返回给调用者,但是解码算法并不在这个方法中实现。

  public DecoderResult decode(BitMatrix bits, Map<DecodeHintType,?> hints)
      throws FormatException, ChecksumException {
    // 构造解析器并读取版本、错误更正级别
    BitMatrixParser parser = new BitMatrixParser(bits);
    FormatException fe = null;
    ChecksumException ce = null;
    try {
      return decode(parser, hints);
    } catch (FormatException e) {
      fe = e;
    } catch (ChecksumException e) {
      ce = e;
    }
    try {
      // 还原位矩阵,在读取码字时还原已完成的掩码移除。
      parser.remask();
      // 为镜像操作准备解析器。将尝试镜像读取版本和格式信息。
      parser.setMirror(true);
      // 从二维码中的两个位置之一读取版本信息。(版本信息在之前的二维码图片中有介绍)
      parser.readVersion();
      // 从二维码中读取格式信息。(格式信息见下图)
      parser.readFormatInformation();
      // 执行到这里意味着我们在镜像时成功地检测到某种版本和格式信息。
      // 镜像位矩阵以尝试二次读取。
      parser.mirror();
      DecoderResult result = decode(parser, hints);
      // 成功!通知调用方代码已镜像。
      result.setOther(new QRCodeDecoderMetaData(true));
      return result;
    } catch (FormatException | ChecksumException e) {
      // 从原始读取中抛出异常
      if (fe != null) {
        throw fe;
      }
      throw ce; 
    }
  }

格式信息有15位,如下图。2 位表示纠错级别,一共有 4 种纠错级别;3 位表示使用何种掩码图案,一共有 8 种掩码图案;10 位纠错信息,使用 BCH 编码 计算得出;上面的 15 位再和固定的101010000010010做异或操作,这样就保证不会因为选用了 00 的纠错级别和 000 的 掩码图案,从而造成全部为白色,这会增加扫描器的图像识别的困难。Dark Module表示一个固定是黑色的块。
在这里插入图片描述

第二个docode方法,DecoderResult decode(BitMatrixParser parser, Map<DecodeHintType,?> hints)。这个方法把从第一个decode接受到的信息进行加工整理,交给第三个decode。

  private DecoderResult decode(BitMatrixParser parser, Map<DecodeHintType,?> hints)
      throws FormatException, ChecksumException {
      //读取版本信息
    Version version = parser.readVersion();
    //读取错误更正级别
    ErrorCorrectionLevel ecLevel = parser.readFormatInformation().getErrorCorrectionLevel();
    // 读码字
    byte[] codewords = parser.readCodewords();
    // 分成数据块
    DataBlock[] dataBlocks = DataBlock.getDataBlocks(codewords, version, ecLevel);
    // 统计数据字节总数
    int totalBytes = 0;
    for (DataBlock dataBlock : dataBlocks) {
      totalBytes += dataBlock.getNumDataCodewords();
    }
    byte[] resultBytes = new byte[totalBytes];
    int resultOffset = 0;
    // 更正错误并将数据块一起复制到字节流中
    for (DataBlock dataBlock : dataBlocks) {
      byte[] codewordBytes = dataBlock.getCodewords();
      int numDataCodewords = dataBlock.getNumDataCodewords();
      correctErrors(codewordBytes, numDataCodewords);
      for (int i = 0; i < numDataCodewords; i++) {
        resultBytes[resultOffset++] = codewordBytes[i];
      }
    }
    // 解码该字节流的内容
    return DecodedBitStreamParser.decode(resultBytes, version, ecLevel, hints);
  }

三、图解解码过程

第三个decode,DecoderResult decode(byte[] bytes,Version version, ErrorCorrectionLevel ecLevel,Map<DecodeHintType,?> hints)。这个方法接收到一系列消息进行解码,并返回最终的编码字符。DecoderResult这个类在com.google.zxing.common中,可以供不同码型的解码器使用。
这个方法中包括了对FNC1、ECI、数字、汉字等不同编码格式的解码,涉及到很多调用,比较杂乱,因此不展示全部代码,通过一个实例来分析源码。首先明确二维码解码顺序:
在这里插入图片描述
下图为解码过程:
在这里插入图片描述

③:去除掩码。以白为 0,以黑为 1,去掩码则是将原二维码与掩码同一坐标的数据做半加法(即 XOR,同色得白,异色得黑)。掩码类型存在DataMask中。
④:确定类别。先看的是右下用的 4 块(黄色框内),编码的顺序为 Z 字型(从右下开始曲折往上),即右下、左下、右上、左上。白记 0,黑记 1。编码类别在Mode
在这里插入图片描述

⑤确定长度。接着再往上看 8 块(紫色框内),同样我们按照 Z 字形的顺序,破译出01001111,转成十进制数为 79。在不同的编码类别(黄框)下,对编码长度的理解不尽相同,这里用的是字母数字模式,编码长度表示的是数量,即本文有 79 个字符。了解这点非常重要,可以告诉你到哪里破译结束。
⑥开始解码:字母数字模式对 45 个字符的字符集进行编码,即:10 个数字 0 ~ 9,26 个大写字母 A ~ Z ,以及 9 个符号 SP、$、%、*、+、-、.、/。通常情况下,两个输入字符用 11 位表示。这些字符会映射成一个字符索引表。如下所示:(其中的 SP 是空格,Char 是字符,Value 是其索引值) 编码的过程是把字符两两分组,然后转成下表的 45 进制,然后转成 11bits 的二进制,如果最后有一个落单的,那就转成 6bits 的二进制。而编码模式和字符的个数需要根据不同的 Version 尺寸编成9, 11 或 13 个二进制。这部分通过类BitSource来实现。注意不要把格式信息编码进去,遇到非数据区绕开或跳过。
在这里插入图片描述
当然,要注意纠错码:
在这里插入图片描述在这里插入图片描述

四、有关算法介绍

一、伽罗华域(Galois Field)上的四则运算

数学知识

1、域
一组元素的集合,以及在集合上的四则运算,构成一个域。其中加法和乘法必须满足交换、结合和分配的规律。加法和乘法具有封闭性,即加法和乘法结果仍然是域中的元素。
域中必须有加法单位元和乘法单位元,且每一个元素都有对应的加法逆元和乘法逆元。但不要求域中的 0 有乘法逆元。

2、有限域
仅含有限多个元素的域。因为它由伽罗华所发现,因而又称为伽罗华域。
所以当我们说伽罗华域的时候,就是指有限域。

GF(2w) 表示含有 2w 个元素的有限域。

3、单位元
Identity Element,也叫幺元(么元),通常使用e来表示单位元。单位元和其他元素结合时,并不会改变那些元素。
对于二元运算 * ,若a * e=a,e称为右单位元;若e * a=a,e称为左单位元,若a* e=e *a=a,则e称为单位元。

4、逆元
对于二元运算 * ,若a * b=e,则a称为b的左逆元素,b称为a的右逆元素。若a * b=b * a=e,则称a为b的逆元,b为a的逆元。

5、本原多项式
域中不可约多项式是不能够进行因子分解的多项式, 本原多项式 (primitive polynomial)是一种特殊的不可约多项式。当一个域上的本原多项式确定了,这个域上的运算也就确定了。本原多项式一般通过查表可得,同一个域往往有多个本原多项式。
通过将域中的元素化为多项式形式,可以将域上的乘法运算转化为普通的多项式乘法再模本原多项式。

Zxing中的GenericGF算法就是使用给定的原始多项式创建一个GF(size)的表示,并规定了一系列运算法则。

伽罗瓦域

下面简单介绍伽罗瓦域:有限域是一组数字,一个域需要有六个属性:封闭性,结合律,交换律,分配律,单位元和可逆。更简单地说,使用域允许研究该域的数字之间的关系,并将结果应用于遵循相同属性的任何其他域。例如,实数集ℝ是一个域。换句话说,数学的域研究一个数集的构造。

然而,整数ℤ不是一个域,因为正如我们上面所说的,并非所有的除法都被定义(例如7/5),这违反了乘法逆性质(x如7 * x = 5不存在)。解决这个问题的一个简单方法是使用素数来取模,比如2:这样,我们保证存在7 * x = 5,因为我们只是环绕一下。ℤ对2取模被称为伽罗瓦域,任何可被2整除的数都是伽罗瓦域(因为我们需要使用素数取模),所以256,8位码元的值,可以减少到2 ^ 8 ,因此我们说我们使用2 ^ 8的伽罗瓦域,或GF(2 ^ 8)。 在口语中,2是该域的特征,8是指数,256是该域的基数。关于有限域的更多信息可以在这里找到。

在这里我们将定义通常用于整数运算的数学运算,但适用于GF(2 ^ 8),它基本上是按照常规运算进行的,但是对2 ^ 8取模。

考虑GF(2)和GF(2 8)之间关系的另一种方法,是将GF(2 8)看做表示8个二进制系数的多项式。例如,在GF(2 8)中,170等于10101010 = 1 * x 7 + 0 * x 6 + 1 * x 5 + 0 * x 4 + 1 * x 3 + 0 * x 2 + 1 * x + 0 = x7 +5 +x 3+ x。两种表示方式都是等价的,只是在第一种情况下,170是十进制的表示,而另一种情况是二进制的表示形式,可以将其视为按照惯例表示多项式(仅在GF(2 p)中使用,如解释这里)。后者通常是学术书籍和硬件实现中使用的表示形式(因为逻辑门和寄存器在二进制级别工作)。对于软件实现来说,优先使用十进制表示,因为这种方式更清晰,更贴近数学的表示(这是我们将在本教程中使用的表示法,除了一些示例将使用二进制表示之外)。

在任何时候,尽量不要将表示单个GF(2 p)码元的多项式(每个系数是一个位/布尔值:0或1),与表示一组GF(2 p)码元列表的多项式(在这种情况下,多项式等价于消息 + RS码,每个系数是介于0和2 p之间的值,并表示消息 + RS码的一个字符)混淆。我们将首先描述单个码元的操作,然后描述码元列表上的多项式操作。

  public GenericGF(int primitive, int size, int b) {
    this.primitive = primitive;
    this.size = size;
    this.generatorBase = b;

    expTable = new int[size];
    logTable = new int[size];
    int x = 1;
    for (int i = 0; i < size; i++) {
      expTable[i] = x;
      x *= 2; 
      if (x >= size) {
        x ^= primitive;
        x &= size - 1;
      }
    }
    for (int i = 0; i < size - 1; i++) {
      logTable[expTable[i]] = i;
    }
    // logTable[0] == 0 但是应该不使用
    zero = new GenericGFPoly(this, new int[]{0});
    one = new GenericGFPoly(this, new int[]{1});
  }

加法和减法
在基于2的伽罗瓦域中,加法和减法都被替换为异或。这是合乎逻辑的:加模2与XOR完全相同,减模2与加模2完全相同。这是可能的,因为加法和减法在这个伽罗瓦域是无进位的。
将我们的8位值看作系数为mod 2的多项式:
0101 + 0110 = 0101 - 0110 = 0101 XOR 0110 = 0011
以相同的方式(以两个GF(2 8)整数的二进制表示):
  ( x2 + 1) + (x2 + x) = 2 x2 + x + 1 = 0 x2 + x + 1 = x + 1
由于(a ^ a)= 0,每个数都是自身取反,所以(x - y)与(x + y)相同。

  • 代码:
  //实现加法和减法——它们在GF(size)中是相同的。
  static int addOrSubtract(int a, int b) {
    return a ^ b;
  }

乘法
  
乘法同样基于多项式乘法。 简单地将输入写成多项式,然后用分配法将它们相乘。举例来说,10001001倍的00101010计算如下。
在这里插入图片描述
同样的结果可以通过修改版本的标准中学乘法过程来获得,其中我们用异或代替加法。

       10001001
*      00101010
      10001001
^   10001001
^ 10001001
  1010001111010

注意:这里的XOR乘法是没有进位的,如果进行了进位,会得到错误的结果1011001111010,而不是正确的结果1010001111010。

  • 代码:
// a与b在GF中的乘积(大小)
  int multiply(int a, int b) {
    if (a == 0 || b == 0) {
      return 0;
    }
    return expTable[(logTable[a] + logTable[b]) % (size - 1)];
  }

//a的乘法逆矩阵
  int inverse(int a) {
    if (a == 0) {
      throw new ArithmeticException();
    }
    return expTable[size - logTable[a] - 1];
  }

log2

//计算log2a
  int log(int a) {
    if (a == 0) {
      throw new IllegalArgumentException();
    }
    return logTable[a];
  }

指数

  int exp(int a) {
    return expTable[a];
  }

二、Reed-Solomon解码

在第二个decode中使用到了这个纠错算法。

原理解释

Reed-Solomon编码(里德-所罗门编码)纠错技术,其基本思想给定n个原始数据块(D1,D2,D3,…,Dn),RS编码根据这n个数据块计算生成m个冗余元素(校验块C1,C2,…,Cm)。从这m+n个数据块中任取n个数据块均能解码出原始数据块,即对于n个数据进行RS编码后生成n+m个数据,能够容忍丢失至多m个数据。
实现的功能听上去很强大,其实现原理却十分简单,其很巧妙地运用了矩阵运算的特点。对于需要进行冗余处理的n个原始数据,写成列向量形式(D),左边生成一个变换矩阵,这个矩阵由n+m行和n列组成,其中上面的n×n的部分是一个单元矩阵,保证原数据在编码后不发生变化,下面的m×n的部分是一个范德蒙矩阵,生成冗余纠错数据。使用范德蒙矩阵是为了保证这个矩阵任取n×n都部分可逆。
在这里插入图片描述
假设丢失了m个数据(包括原始数据和纠错数据),比如下图中D1、D4和C2丢失,需要从剩余的n个数据中恢复出原始数据D1 - Dn。从编码矩阵B中删除丢失数据和丢失编码对应行,将剩余的数据挑出来得到新n×n的矩阵B’。因为编码矩阵B的任意n行组成的矩阵都可逆,所以根据剩余的有效数据矩阵,即可把中间原始数据矩阵D解出来了。
在这里插入图片描述
面的方法理论上能够做到数据冗余处理,不过由于作为一种编码技术,RS编码需要处理的是特定长度的二进制数据,然而求矩阵逆的过程是在实数域中进行的。显然特定长度的二进制是无法准确描述实数的。因此如何构造编码矩阵B成为关键。为了解决这个问题,RS的计算域采用能够用二进制精确编码的伽罗华域GF(2n)(有限域)。

公式

RS过程是作用在用户选择的域F(通常为在这里插入图片描述,这个公式的解释见上面伽罗华域)里的,目的是为了计算机处理的方便。
1.假定我们接受到的码字是在这里插入图片描述其中每个值都是Galois Field中的元素,那么,先直接定义一下接收码字多项式:在这里插入图片描述
2.在接收端,我们并不知道真实发送的值在这里插入图片描述是多少。但是为了要解决问题,先定义一下误差。误差是指实际值与真实值之间的差值在这里插入图片描述然后给出误差多项式(明确一点,误差目前是未知的):在这里插入图片描述
3.定义典型值在这里插入图片描述它是以 [公式] ( α 是generator,域F里的数值必需可以表示成在这里插入图片描述,一般α=2)为参数代入到接收码字多项式函数 [公式]
中去得到的。典型值的个数为m,0<=i<=m。在这里插入图片描述在上式中,认为在这里插入图片描述这是因为在这里插入图片描述在这里插入图片描述就是说,s可以认为是g的倍数,而在这里插入图片描述由此,我们可以看出来,典型值只和消息码字传输过程中产生的误差相关。
如果,所有的典型值都是0,那么说明接收到的码字是无误差的,就是原始消息码字。如果典型值里有不是0的怎么办?看下一步
4.把所有典型值表达式写成一个方程组的形式
在这里插入图片描述
5.再改写成矩阵形式表达
在这里插入图片描述
6.最后把误差项系数单独列出来
在这里插入图片描述
α是已知的,Si是已知的,但由于n<m,方程组是欠定的,因此还不能求出误差e
7.假设接收的消息多项式中有v 项是有错的,它的范围满足 [公式]接下来,我们还要假设代表错码位置的变量:在这里插入图片描述它们满足在这里插入图片描述并且各个 Ii之间是没有顺序的。有了误差位置变量,那么在这些位置上的误差可以表示为:在这里插入图片描述这个序列里的误差是有可能存在为0的值的,但是码字多项式其它位置上的系数误差ei一定是0,因为我们已经定义了有误差的位置只能发生在Ii上。继续定义新的变量,为了后面的计算方便:在这里插入图片描述这两个新的变量,可以理解为:X 是错码位置上的变量x项,Y是错码位置上系数项。由于除了错码位置外的其它项都是没错的ei=0,所以上面的典型值矩阵方程可以进行化相应的化简,再用新变量Xi与Yi替换老的变量,可以得到在这里插入图片描述
8.构造定位多项式
在这里插入图片描述
9.当0<=i<v时
在这里插入图片描述由于在这里插入图片描述
10。由上面的0等式,两边同乘以在这里插入图片描述在这里插入图片描述
11.对于每一种在这里插入图片描述的等式,将它们相加:
在这里插入图片描述

12.再继续调整项的位置,其中在这里插入图片描述
在这里插入图片描述
13.为每一种j列一个等式:
在这里插入图片描述
14.用矩阵的形式重新表达一下:
在这里插入图片描述
15.构造一个增广矩阵:
在这里插入图片描述将它送入Gauss-Jordan求解器就可以得到在这里插入图片描述在这里插入图片描述的多项式具体表达就出来了。接下来,将在这里插入图片描述代入这个多项式,如果出现 在这里插入图片描述那么说明接收消息多项式的第 i的位置有误差,即ei不等于0.通过这种方式,我们就找到了所有误差项所在位置:在这里插入图片描述我们知道在这里插入图片描述在这里插入图片描述在这里插入图片描述,再列下之前的纠错方程组:
在这里插入图片描述S可以通过r(x)计算得到,Y就可以求得了。
16.有了出错项的位置和对应项的误差值,就可以复原原始信息值:
在这里插入图片描述
17.最后需要用典型多项式在验证一下解码纠错是否成功。

举例

RS码简介和编译码算法综述.ppt中有一些例子,可以方便理解。

欢迎提出宝贵意见,感谢观看!
参考: ZxingAPI
ISO/IEC 18004-2015 二维码标准文档
如何笔算解码二维码?
所罗门编码
Reed-Solomon算法
二维码学习笔记

  • 5
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
非常抱歉,我之前给出的示例中有误。C++版的ZXing库中没有名为 `HybridBinarizer` 的命名空间 `zxing` 成员。请忽略我之前的回答。 在C++版的ZXing库中,要使用混合二值化(Hybrid Binarization)算法,你可以使用 `zxing::GlobalHistogramBinarizer` 类来替代。下面是一个修正后的示例: ```cpp #include <iostream> #include <zxing/DecodeHints.h> #include <zxing/MultiFormatReader.h> #include <zxing/Result.h> #include <zxing/BinaryBitmap.h> #include <zxing/common/GlobalHistogramBinarizer.h> int main() { // 加载图像 zxing::Ref<zxing::LuminanceSource> source = zxing::FileLuminanceSource::create("path/to/your/image.jpg"); zxing::Ref<zxing::Binarizer> binarizer = zxing::Ref<zxing::Binarizer>(new zxing::GlobalHistogramBinarizer(source)); zxing::Ref<zxing::BinaryBitmap> bitmap = zxing::Ref<zxing::BinaryBitmap>(new zxing::BinaryBitmap(binarizer)); // 设置解码提示 zxing::DecodeHints hints; hints.setTryHarder(true); // 解码二维码 zxing::MultiFormatReader reader; zxing::Ref<zxing::Result> result = reader.decode(bitmap, hints); // 提取解码结果 std::string decodedData = result->getText()->getText(); std::cout << "Decoded data: " << decodedData << std::endl; return 0; } ``` 这个修正后的示例使用了 `zxing::GlobalHistogramBinarizer` 来进行图像的二值化处理,替代了之前错误的 `HybridBinarizer`。 请确保你已正确安装了ZXing库,并将其包含路径添加到你的项目配置中,以便编译器能够找到正确的头文件和库文件。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值