笔者最近在研究二维码相关的东西,正好有涉及到Reed-Solomon码,这里简单写一下笔记。有不当之处请评论区指正,欢迎理性讨论。
【参考资料】
Reed–Solomon codes for coders - Wikiversity
网址:https://en.wikiversity.org/wiki/Reed%E2%80%93Solomon_codes_for_coders
意义、优势、应用等暂时略过。
基础部分可以看我之前写的 基础知识。这里只在它的基础上加以补充(虽然前一篇也没写多少东西)
热身例子
假设B有个字典,里面有三个单词:this that 和 corn。A向B发送的信息只能是字典中的一个单词。
某次当A向B发送信息时,某些位被破坏,变成了星号,所以B只收到一个词 co**,那么根据字典,B可以知道,corn与co**最相似,所以原来的词是corn。
但是如果B收到的词是th**,则B无法确定是this还是that。因此,这个字典应该选取彼此更不一样的词,以便于信息损坏后能更准确地恢复。
那么,字典中任意两个词之间差异的位数,我们称为汉明距离【Hmming distance】,这其中的最小值,称为字典的最大汉明距离【maximum Hamming distance】。
确保字典中任意两个词都只在几个(值为最大汉明距离)位置上不同,称为最大分离度【maximum separability】。
对于现在的字典,为了增加最大汉明距离,可以增加所有字条的长度,如改为:
t h i s a b c d
t h a t b c d e
c o r n c d e f
那么,当 1)有4个损坏到来时,我们能知道原码是 t h i s a b c d。
t * * * a b * d
当 2)有两个错码到来时,我们能知道原码是 t h a t b c d e。
t h o s b c d e
Reed-Solomon编码的核心思想也与此相似:在伽罗华域【Galois Field】的数据后,加入一些冗余码。解码的原始思想也是在一个集合里查找最相似的数据,不过为了能编解码更长的信息,在算法方面做了优化。
二维码基础
二维码有自己的结构,如下图所示,很多部分是跟版本(尺寸)绑定不变的。
编码步骤是:1)数据分析;2)数据编码;3)纠错码编制;4)数据交错和补充;5)填充矩阵;6)掩码;7)填充格式和版本信息。其中掩码是为了让黑块和白块分布地更均衡,尽量避免某些图案的出现。
如下图所示,右侧是掩码前的原码。红色区域是编码格式信息的,浅灰色是format information和timing patterns,也就是已经固定的信息,不是实际要传输的数据信息。实际要传的、已经编码好了的,是黑色区域。白色表示0,黑色表示1。
而掩码本身,就是拿一个相同尺寸的矩阵,去做异或。掩码有很多种模式,需要选择效果最好的。
由于二维码被扫描时受光线、印刷质量等的影响,所以难免有损失。为了解码时能更可靠地解码,我们先引入BCH译码器,再细化到Reed-Solomon译码器。
BCH检错
如果确定二维码编码的结果是对的呢?假设生成二维码的generator是101 0011 0111,编码的结果是000 1111 0101 1001,那么用编码结果除以generator(长除法,做异或而非【减】),余数应该是0的。(为什么应该是0,参考下面有限域知识)下面展示的是bch的解码能力。
def qr_check_format(fmt):
g = 0x537
for i in range(4, -1, -1):
if fmt & (1 << (i+10)):
fmt ^= (g << i)
return fmt
# 实际是计算码里有多少1
def hamming_weight(x):
weight = 0
while x > 0:
weight += x & 1
x >>= 1
return weight
def qr_decode_format(fmt):
best_fmt = -1
best_dist = 15
for test_fmt in range(0,32):
test_code = (test_fmt<<10)^qr_check_format(test_fmt<<10)
test_dist = hamming_weight(fmt^test_code)
if test_dist < best_dist:
best_dist = test_dist
best_fmt = test_fmt
elif test_dist == best_dist:
best_fmt = -1
#print("best_fmt " + str(best_fmt))
return best_fmt
print(qr_decode_format(int("000111101011001",2))) # 3
# 分别错1, 2, 3位, min(Hamming distance)都是3
print(qr_decode_format(int("100111101011001",2))) # 3
print(qr_decode_format(int("010101101011001",2))) # 3
print(qr_decode_format(int("111111101011001",2))) # 3
# 错4位时有至少两个是等最小距离的,所以无法单一解码
print(qr_decode_format(int("111011101011001",2))) # -1
有限域知识
阿贝尔群(交换群):满足结合律、交换律;群有单位元;所有元素有逆元。
环:1.加法上是阿贝尔群;2.乘法满足结合律;3.对加法和乘法有分配律。
域:能进行加、减、乘、除的代数结构。计算结果不会超出域的集合。其中乘法应该满足交换律,否则则称为【体】。
有限域:域的元素个数有限,元素的个数称为【阶】。
**伽罗华域:**每个有限域的阶必须为素数的幂,可表示为 p ^ n,这样的有限域称为Galois域,记作GF(p ^ n)。其上做完加法和乘法后,要对p^n取模,以便结果仍是域中的元素。
对于GF(2^n),用多项式表示的话,系数只能用0或1,因此在计算机领域很方便。举例:GF (2 ^ 8)中,170用二进制表示是10101010,那么多项式为:
0101010 = 1*x^7 + 0*x^6 + 1*x^5 + 0*x^4 + 1*x^3 + 0*x^2 + 1*x + 0
= x^7 + x^5 + x^3 + x
同时,GF(2 ^ n)上,加和减是一样的,都是按位做异或。
def gf_add(x, y):
return x ^ y
def gf_sub(x, y):
return x ^ y
乘法则是多项式乘法,或者直接移位异或,如下图所示:
def cl_mul(x, y):
z = 0
i = 0
while (y>>i) > 0:
if y & (1<<i):
z ^= x<<i
i += 1
return z
# 0b1010001111010
print(bin(cl_mul(0b10001001, 0b00101010)))
那么,既然要放到有限域,太长的话是不行的,要取模。这里的除数称为生成式,为啥选它后面会说,现在只告诉你要选一个 1 0001 1101 这样的东西取模。完整的计算:
def gf_multi_noLUT(x, y, prim=0):
def cl_mul(x, y):
z = 0
i = 0
while (y>>i) > 0:
if y & (1<<i):
z ^= x<<i
i += 1
return z
# 类似 int(4).bit_length(),就是算这个数字用几个二进制位可以表示
def bit_length(n):
bits = 0
while n >> bits: bits += 1
return bits
# 这里模拟的是长除法
def cl_div(dividend, divisor=None):
dl1 = bit_length(dividend)
dl2 = bit_length(divisor)
if dl1 < dl2:
return dividend
for i in range(dl1-dl2, -1, -1):
if dividend & (1<<(i+dl2-1)):
dividend ^= divisor << i
return dividend
result = cl_mul(x, y)
if prim > 0:
result = cl_div(result, prim)
return result
a = 0b10001001
b = 0b00101010
# 跟上面一样
print(bin(gf_multi_noLUT(a, b, 0))) # 0b1010001111010
# 0x11d就是1 0001 1101
print(bin(gf_multi_noLUT(a, b, 0x11d))) # 0b11000011
【未完待续】