二维码详解(QR Code)

做者:王子旭
连接:https://zhuanlan.zhihu.com/p/21463650
来源:知乎
著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。

2016.7.5 更新:长文多图代码预警,电脑食用效果更佳。php

完整版代码已上传 GitHub,后续一些有的没的的代码更新也都在GitHub上(python

给结尾的几个被自动识别的QR码作了防自动识别。。顺便也检测一下咱们这不怎么高的容错率(7%)。要是再被知乎自动识别了。。。_(:з」∠)_git

======================================================================github

做为一只程序猿,第一篇文章天然要写hello world,可是呐,我看大家今天这样热情,只写一句hello world就闷声大家又不高兴。恰好最近实习工做在处理QR码,就来薛习一下QR码版本的hello world吧。算法

  • 前期准备

  • 背景信息shell

要想实现一个QR码生成器,咱们首先须要了解什么是QR码,QR码有哪些类型,以及QR码是如何工做的。app

QR码(Quick Response Code) 是二维码的一种,在正方形二位矩阵内经过黑白标识编码二进制位从而编码数据,最先发明用于日本汽车制造业追踪零部件。QR码现有40个标准版本,4个微型版本。QR码的数据编码方式有四种:less

  1. 数字(Numeric):0-9
  2. 大写字母和数字(alphanumeric):0-9,A-Z,空格,$,%,*,+,-,.,/,:
  3. 二进制/字节:经过 ISO/IEC 8859-1 标准编码
  4. 日本汉字/假名:经过 Shift JISJIS X 0208 标准编码

QR码还有四种容错级别能够选择:ide

  1. L(Low):7%的字码可被修正
  2. M(Medium):15%的字码可被修正
  3. Q(Quartile):25%的字码可被修正
  4. H(High):30%的字码可被修正

(Wikipedia: QR code, )函数

(40+4)×4×4=...... ∑(っ °Д °;)っ咳。。好,那咱们为了读者着想 (←_←),只实现 Version 1-Byte mode-Low error control 的QR码生成就行了嗯。。

好咱们继续。

现在QR码随处可见,你们阅码无数可能也发现了一些规律:这些QR码有大有小、有红有绿,有些还有各类装饰,可是它们总有一些部分看起来十分类似,好比三个角落里总有“回”字形的图样。这就要谈到QR码的结构了。

  • 结构

除了存储编码的数据,QR码里还含有一些基本标准里钦定的图样来帮助扫描软件快速识别和解码。(图片来源:Wikipedia:QR码,

标准(ISO/IEC 18004)里是这样说的(图片来源:ISO/IEC 18004: Information – Automatic identification and data capture techniques – QR Code barcode symbology specification

因此说咱们作QR码啊,仍是要按照QR标准,按照基本标准来。我没有任何硬点这些图样的意思,它们都是有本身的做用的,咱们一个一个说。

    • 功能性图样(function patterns):不参与编码数据的区域。

      • 闷声区(quite zone):标准中规定标准QR码(Ver1-40)四周应有宽4个单位、微型QR码四周应有宽2个单位的区域颜色等效于QR码中白色点(light module),其中不能有图样或标记,以保证QR码清晰可识别。
      • 定位标识(finder pattern):以前提到的“回”字形标识,位于QR码的左上,右上和左下角,用于协助扫描软件定位QR码并变换坐标系。定位标识可让QR码在任意角度被扫描,这是一维条形码作不到的。(图片来源:ISO/IEC 18004: Information – Automatic identification and data capture techniques – QR Code barcode symbology specification
      • 分隔符(separator):一单位宽的白色点带,位于每一个定位标识和编码区域之间用于区分。
      • 定时标识(timing pattern):一单位宽的黑白交替点带,由黑色起始和结束,用于指示标识密度和肯定坐标系。
      • 校订标识(alignment pattern):只有 Version 2 及以上的QR码有校订标识。校订标识用于进一步校订坐标系。校订标识的数量取决于版本。
    • 编码区域(encoding region):编码数据的区域。
      • 格式信息(format information):存储容错级别和数据掩码,和额外的自身BCH容错码,讲到再展开。
      • 版本信息(version information):存储版本信息。
      • 数据及容错字码(data and error correction codewords):存储编码方式,实际编码的数据和数据的RS容错码。

以上就是QR码的通用结构标准了,再来看一看咱们要实现的 Version 1 QR码的结构:

(图片来源:ISO/IEC 18004: Information – Automatic identification and data capture techniques – QR Code barcode symbology specification

分析完了QR码的结构,豁然开朗,这东西也不就这么回事嘛,简单!开始作!(多年之后,当程序猿面对电脑屏幕的时候,将会回想起不懂事的本身立起flag的那个下午)

  • 流程

方便的是,标准也规定了将数据编码成QR码的流程:

  1. 数据分析(data analysis):分析输入数据,根据数据决定要使用的QR码版本、容错级别和编码模式。低版本的QR码没法编码过长的数据,含有非数字字母字符的数据要使用扩展字符编码模式。由于咱们只实现 V1-L byte mode QR码,此步略去。
  2. 编码数据(data encoding):根据选择的编码模式,将输入的字符串转换成比特流,插入模式标识码(mode indicator)和终止标识符(terminator),把比特流切分红八比特的字节,加入填充字节来知足标准的数据字码数要求。
  3. 计算容错码(error correction coding):对步骤二产生的比特流计算容错码,附在比特流以后。高版本的编码方式可能须要将数据流切分红块(block)再分别进行容错码计算。
  4. 组织数据(structure final message):根据结构图把步骤三获得的有容错的数据流切分,准备填充。
  5. 填充(module placement in matrix):把数据和功能性图样根据标准填充到矩阵中。
  6. 应用数据掩码(data masking):应用标准中的八个数据掩码来变换编码区域的数据,选择最优的掩码应用。讲到再展开。
  7. 填充格式和版本信息(format and version information):计算格式和版本信息填入矩阵,完成QR码。

简单! ( ̄ε(# ̄)☆╰╮( ̄▽ ̄///) ↓↓↓

  • 代码实战


为(yin)了(wei)可(wo)读(lan)和简单,咱们用 Python 来实现这个简化版QR码生成器。为了生成和操做图像,咱们须要安装第三方图像处理库 Python Imaging Library (PIL) 。限于篇幅,本文不对PIL的使用作过多介绍,入门可参见 PIL - 廖雪峰的官方网站Python Imaging Library Handbook

由于程序须要处理矩阵,方便起见在这里先定义坐标系统。以矩阵的左上角为原点,原点坐标定义为(0,0),i 轴向右,坐标 i 对应列;j 轴向下,坐标 j 对应行。因而对于图像中的像素(i,j),有矩阵元素 mat [ j ] [ i ] 与之对应。

新建Python代码文件 qrcode.py,引入须要的库:

# qrcode.py from PIL import Image, ImageDraw 

为了思惟简便,咱们自顶向下地构建代码。首先,假设咱们已经填充好了一个QR码的矩阵bitmap,咱们须要把相应的图像生成出来。这里就有了图像大小的问题:Version 1 的QR码表示为 21×21 的矩阵,直接把这个矩阵当作位图来输出的话,图像只有21像素宽。为了得到大小合适的图像,咱们先定义图像大小,再把每个像素映射到合适的矩阵元素上。

在 qrcode.py 中添加以下代码:

def _genImage(bitmap, width, filename): '''  Generate image corresponding to the input bitmap  with specified width and filename.  ''' # New image in black-white mode initialized with white. img = Image.new('1', (width, width), 'white') drw = ImageDraw.Draw(img) # Normalized pixel width. pwidth = width / len(bitmap) for j in range(width): # Normalized j coordinate in bitmap normalj = j / pwidth for i in range(width): # Normalized i coordinate in bitmap normali = i / pwidth if normalj < len(bitmap) and normali < len(bitmap): # Draw pixel. drw.point((i, j), fill=bitmap[normalj][normali]) img.save(filename) 

这个函数接收三个参数:QR码矩阵bitmap,图像宽度width,保存文件名filename。

img = Image.new('1', (width, width), 'white') drw = ImageDraw.Draw(img) 

这两行初始化了图像和绘图工具。初始化图像时的参数 '1' 表明生成黑白模式图像,'white' 表明图像初始化填充白色。

pwidth = width / len(bitmap) 

用图像宽度除以矩阵维度获得标准化后的像素宽度(QR码中一个单位对应的像素数)。

normalj = j / pwidth normali = i / pwidth 

遍历图像时,将像素坐标(i,j)标准化为矩阵坐标 [ i ][ j ]。检查不越界以后,按坐标绘制像素,最后保存图像。

保存代码后,咱们测试一下这个函数。在文件目录打开命令行/shell,输入python进入Python REPL。引入qrcode而后进行测试。

咱们定义了一个矩阵 test,而后调用 qrcode._genImage 来生成一个240×240,名为 test.jpg 的图像以下。

咱们注意到,原点处对应 (0 + 0) % 2 的像素为黑,由于 0 值对应黑色,1 对应白色。为了明确,在 qrcode.py 中加入以下定义

_LIGHT = 1 _DARK = 0 

可见咱们的图像生成函数是成功的,如今只须要填充出QR码矩阵就好了。

嗯,玩一会。。_(:з」∠)_

好咱们继续。

_genImage 函数接收QR码矩阵做为参数,自顶向下地,咱们须要生成这个矩阵。考虑到一个QR码中有不少不变的图样(fixed pattern),咱们能够预先填充好一个含有这些不变图样的模板,生成QR码矩阵时直接把编码好的数据填充到这个模板里就好了。

在 qrcode.py 中加入模板的定义,待填充:

_ver1 = [[_LIGHT for i in range(21)] for j in range(21)] 

假设 _ver1 是已经填充好的模板,咱们生成QR码矩阵须要怎么作呢?根据前期准备,咱们须要编码数据,填充数据,应用掩码,再填充格式信息。因而咱们定义这些函数:

def _fmtEncode(fmt): '''Encode format code.''' pass def _encode(data): '''  Encode the input data stream.  Add mode prefix, encode data using ISO-8859-1,  group data, add padding suffix, and call RS encoding method.  ''' pass def _fillData(bitstream): '''Fill the encoded data into the template QR code matrix''' pass def _mask(mat): '''  Mask the data QR code matrix with all 8 masks,  and select the best mask.  ''' pass def _fillInfo(arg): '''  Fill the encoded format code into the masked QR code matrix.  ''' pass def _genBitmap(bitstream): '''  Take in the encoded data stream and generate the  final QR code bitmap.  ''' return _fillInfo(_mask(_fillData(bitstream))) 

_encode 编码数据,_fillData 将这些数据填充到模板中,_mask 应用掩码,_fmtEncode 编码格式信息,_fillInfo 填充格式信息,最后 _genBitmap 把这些函数按标准串联起来,返回准备好的QR码矩阵给 _genImage 来生成QR码。

接下来咱们按照流程顺序实现这些函数。

  • 编码数据

首先咱们要检测输入的数据是否超过V1-L byte mode的最大编码长度17,若是超过就抛出异常。在 qrcode.py 开始定义异常:

class CapacityOverflowException(Exception): '''Exception for data larger than 17 characters in V1-L byte mode.''' def __init__(self, arg): self.arg = arg def __str__(self): return repr(self.arg) 

在 _encode 中加入检测

def _encode(data): '''  Encode the input data stream.  Add mode prefix, encode data using ISO-8859-1,  group data, add padding suffix, and call RS encoding method.  ''' if len(data) > 17: raise CapacityOverflowException( 'Error: Version 1 QR code encodes no more than 17 characters.') 

在编码数据以前,还要按照标准的规定加入编码模式前缀和数据字符计数,byte mode的前缀是 0100,接上八位二进制数表明的数据长度,构成数据前缀。再把数据用 ISO/IEC 8859-1 标准编码,按八个二进制位分组,接上终止符和11101100和00010001交替的填充字节,按标准修剪到19字节,完成数据编码。实现 _encode 以下:

def _encode(data): '''  Encode the input data stream.  Add mode prefix, encode data using ISO-8859-1,  group data, add padding suffix, and call RS encoding method.  ''' if len(data) > 17: raise CapacityOverflowException( 'Error: Version 1 QR code encodes no more than 17 characters.') # Byte mode prefix 0100. bitstring = '0100' # Character count in 8 binary bits. bitstring += '{:08b}'.format(len(data)) # Encode every character in ISO-8859-1 in 8 binary bits. for c in data: bitstring += '{:08b}'.format(ord(c.encode('iso-8859-1'))) # Terminator 0000. bitstring += '0000' res = list() # Convert string to byte numbers. while bitstring: res.append(int(bitstring[:8], 2)) bitstring = bitstring[8:] # Add padding pattern. while len(res) < 19: res.append(int('11101100', 2)) res.append(int('00010001', 2)) # Slice to 19 bytes for V1-L. res = res[:19] 

在L容错等级下,编码了数据咱们还须要计算出七位的里德-所罗门码(可简单了,看我和蔼的眼神 )

里德-所罗门码是 定长码。这意味着一个固定长度输入的数据将被处理成一个固定长度的输出数据。在最经常使用的(255,223)里所码中,223个里德-所罗门输入符号(每一个符号有8个 位元)被编码成255个输出符号。
大多数里所错误校订编码流程是成体系的。这意味着输出的码字中有一部分包含着输入数据的原始形式。
符号大小为8位元的里所码迫使码长( 编码长度)最长为255个符号。
标准的(255,223)里所码能够在每一个码字中校订最多16个里所符号的错误。因为每一个符号事实上是8个位元,这意味着这个码能够校订最多16个短爆发性错误。

里德-所罗门码,如同 卷积码同样,是一种透明码。这表明若是信道符号在队列的某些地方被反转,解码器同样能够工做。解码结果将是原始数据的补充。可是,里所码在缩短后会失去透明性。在缩短了的码中,“丢失”的比特须要被0或者1替代,这由数据是否须要补足而决定。(若是符号这时候反转,替代的0须要变成1)。因而乎,须要在里所解码前对数据进行强制性的侦测决定(“是”或者“补足”)。

(Wikipedia: 里德-所罗门码,

......................................(⊙v⊙).....................................

这。。仍是留给有兴趣的读者吧(微笑)

参考:Reed–Solomon codes for coders

在 _encode 以前加入以下RS容错码计算工具:

def _gfpMul(x, y, prim=0x11d, field_charac_full=256, carryless=True): '''Galois field GF(2^8) multiplication.''' r = 0 while y: if y & 1: r = r ^ x if carryless else r + x y = y >> 1 x = x << 1 if prim > 0 and x & field_charac_full: x = x ^ prim return r # Calculate alphas to simplify GF calculations. _gfExp = [0] * 512 _gfLog = [0] * 256 _gfPrim = 0x11d _x = 1 for i in range(255): _gfExp[i] = _x _gfLog[_x] = i _x = _gfpMul(_x, 2) for i in range(255, 512): _gfExp[i] = _gfExp[i-255] def _gfPow(x, pow): '''GF power.''' return _gfExp[(_gfLog[x] * pow) % 255] def _gfMul(x, y): '''Simplified GF multiplication.''' if x == 0 or y == 0: return 0 return _gfExp[_gfLog[x] + _gfLog[y]] def _gfPolyMul(p, q): '''GF polynomial multiplication.''' r = [0] * (len(p) + len(q) - 1) for j in range(len(q)): for i in range(len(p)): r[i+j] ^= _gfMul(p[i], q[j]) return r def _gfPolyDiv(dividend, divisor): '''GF polynomial division.''' res = list(dividend) for i in range(len(dividend) - len(divisor) + 1): coef = res[i] if coef != 0: for j in range(1, len(divisor)): if divisor[j] != 0: res[i+j] ^= _gfMul(divisor[j], coef) sep = -(len(divisor) - 1) return res[:sep], res[sep:] def _rsGenPoly(nsym): '''Generate generator polynomial for RS algorithm.''' g = [1] for i in range(nsym): g = _gfPolyMul(g, [1, _gfPow(2, i)]) return g def _rsEncode(bitstring, nsym): '''Encode bitstring with nsym EC bits using RS algorithm.''' gen = _rsGenPoly(nsym) res = [0] * (len(bitstring) + len(gen) - 1) res[:len(bitstring)] = bitstring for i in range(len(bitstring)): coef = res[i] if coef != 0: for j in range(1, len(gen)): res[i+j] ^= _gfMul(gen[j], coef) res[:len(bitstring)] = bitstring return res 

(Source: Wikiversity:Reed–Solomon codes for coders

在 _encode 结尾直接调用 _rsEncode 添加容错码,完成数据编码部分。

def _encode(data): '''  Encode the input data stream.  Add mode prefix, encode data using ISO-8859-1,  group data, add padding suffix, and call RS encoding method.  ''' if len(data) > 17: raise CapacityOverflowException( 'Error: Version 1 QR code encodes no more than 17 characters.') # Byte mode prefix 0100. bitstring = '0100' # Character count in 8 binary bits. bitstring += '{:08b}'.format(len(data)) # Encode every character in ISO-8859-1 in 8 binary bits. for c in data: bitstring += '{:08b}'.format(ord(c.encode('iso-8859-1'))) # Terminator 0000. bitstring += '0000' res = list() # Convert string to byte numbers. while bitstring: res.append(int(bitstring[:8], 2)) bitstring = bitstring[8:] # Add padding pattern. while len(res) < 19: res.append(int('11101100', 2)) res.append(int('00010001', 2)) # Slice to 19 bytes for V1-L. res = res[:19] # Call _rsEncode to add 7 EC bits. return _rsEncode(res, 7) 

  • 数据切分和填充

(在我完成这个项目以后,想了想数据填充有更优雅的方式,还能够通用在其余版本的QR码上。感兴趣或者是想到的读者能够自行实现优化的 _fillData)

QR码标准将八个二进制位(一字节)规定为一个数据元组,先将编码后数据的每个字节填充到 2×4 的矩阵(高版本QR码中会出现不规则形状的字节元组,本文中不考虑。)中,再将这些小的矩阵填入QR码矩阵。标准也规定了字节填入小矩阵的方式:

(图片来源:ISO/IEC 18004: Information – Automatic identification and data capture techniques – QR Code barcode symbology specification

其中,7表明字节最高位(most significant bit),0表明最低位(least significant bit)。

在 _fillData 前添加 _fillByte 来实现单个字节的填充:

def _fillByte(byte, downwards=False): '''  Fill a byte into a 2 by 4 matrix upwards,  unless specified downwards.  ''' bytestr = '{:08b}'.format(byte) res = [[0, 0], [0, 0], [0, 0], [0, 0]] for i in range(8): res[i/2][i%2] = not int(bytestr[7-i]) if downwards: res = res[::-1] return res 

有了填充好的小矩阵,接下来就把它们填入大矩阵中。标准规定的填充方式为:由大矩阵的右下开始向上填充,遇到编码区域的边界后向左,改成向下填充,如此蛇行将数据填入数据区域。

(图片来源:Wikipedia:QR code,

(图片来源:ISO/IEC 18004: Information – Automatic identification and data capture techniques – QR Code barcode symbology specification

考虑到将小矩阵填入大矩阵的操做会很是频繁,咱们把它写成函数来实现复用。在 qrcode.py 开始添加函数

def _matCp(src, dst, top, left):
    '''
    Copy the content of matrix src into matrix dst.
    The top-left corner of src is positioned at (left, top)
    in dst.
    '''
    res = copy.deepcopy(dst)
    for j in range(len(src)):
        for i in range(len(src[0])):
            res[top+j][left+i] = src[j][i]
    return res

要实现 _fillData,咱们就会用到以前说的模板矩阵,那咱们先把模板矩阵填充出来吧。

咱们的想法是在模板矩阵中填入在全部V1-L QR码中都固定不变的标识来简化生成过程,那么首先咱们得找出全部这样固定不变的标识。以前提到的功能性标识包含了大部分固定的图样,那么咱们先填充出这些功能性标识。定位标识和校订标识能够定义为变量,可是定时标识会随版本变化有长度变化,为了代码的可扩展性,咱们把定时标识定义为生成函数。

在 qrcode.py 开始添加定义:

def _transpose(mat):
    '''Transpose a matrix'''
    res = [[mat[j][i] for j in range(len(mat))] for i in range(len(mat[0]))]
    return res

def _timSeq(len, vertical=False):
    '''
    Generate a horizontal, unless specified vertical
    timing sequence with alternating dark and light
    pixels with length len.
    '''
    res = [[i % 2 for i in range(len)]]
    if vertical:
        res = _transpose(res)
    return res

# Finder pattern.
_finder = _matCp(_matCp([[_DARK for i in range(3)] for j in range(3)],
    [[_LIGHT for i in range(5)] for j in range(5)], 1, 1),
    [[_DARK for i in range(7)] for j in range(7)], 1, 1)

# Alignment pattern. Not used in version 1.
_align = _matCp(_matCp([[_DARK]],
    [[_LIGHT for i in range(3)] for j in range(3)], 1, 1),
    [[_DARK for i in range(5)] for j in range(5)], 1, 1)

有了这些功能性标识,先别急着往模板里填。仔细读标准咱们会发现,在格式信息区域也有一个固定不变的黑点。

(图片来源:ISO/IEC 18004: Information – Automatic identification and data capture techniques – QR Code barcode symbology specification

实际上这张图里就是 Version 1 QR码里所有的不变样式了。继续在 qrcode.py 中填充模板:

# Version 1 QR code template with fixed patterns. _ver1 = [[_LIGHT for i in range(21)] for j in range(21)] _ver1 = _matCp(_finder, _ver1, 0, 0) _ver1 = _matCp(_finder, _ver1, 14, 0) _ver1 = _matCp(_finder, _ver1, 0, 14) _ver1 = _matCp(_timSeq(5), _ver1, 6, 8) _ver1 = _matCp(_timSeq(5, vertical=True), _ver1, 8, 6) _ver1 = _matCp([[_DARK]], _ver1, 13, 8) 

咱们的模板矩阵就完成了,效果如图:

为了不填充过程修改模板而致使后续QR码生成出错,保险起见咱们只经过deepcopy使用这个模板,在 qrcode.py 头部加入模块引入:

import copy 

而后实现 _fillData 以下:

def _fillData(bitstream): '''Fill the encoded data into the template QR code matrix''' res = copy.deepcopy(_ver1) for i in range(15): res = _matCp(_fillByte(bitstream[i], (i/3)%2!=0), res, 21-4*((i%3-1)*(-1)**((i/3)%2)+2), 21-2*(i/3+1)) tmp = _fillByte(bitstream[15]) res = _matCp(tmp[2:], res, 7, 11) res = _matCp(tmp[:2], res, 4, 11) tmp = _fillByte(bitstream[16]) res = _matCp(tmp, res, 0, 11) tmp = _fillByte(bitstream[17], True) res = _matCp(tmp, res, 0, 9) tmp = _fillByte(bitstream[18], True) res = _matCp(tmp[:2], res, 4, 9) res = _matCp(tmp[2:], res, 7, 9) for i in range(3): res = _matCp(_fillByte(bitstream[19+i], True), res, 9+4*i, 9) tmp = _fillByte(bitstream[22]) res = _matCp(tmp, res, 9, 7) for i in range(3): res = _matCp(_fillByte(bitstream[23+i], i%2==0), res, 9, 4-2*i) return res 

这是一个很是ad hoc的实现,代码长可是没有什么技术含量。

测试一下填入数据的效果:

已经有一些QR码的样子了! <( ̄︶ ̄)> (没有完成,这是没法扫描的)

  • 掩码和惩♂罚

获得了填入数据的矩阵,下一步就是应用掩码来变换数据图样。那有人要问了,既然咱们已经把数据编入了QR码,想编码的信息就已经在里面了,为何不直接填入格式信息获得QR码,而要多进行这么一步操做呢?

掩码真的是画蛇添足吗?大家呐仍是要提升自身的姿式水平。QR码是要拿来扫描的,而扫描怕的就是没法清晰地分辨出编码信息的每一位。要是QR码中黑白点数量不均,或是空间分布不均都会致使大色块区域的出现,而大色块区域的出现会增长扫描时定位的难度,从而下降扫描的效率。更严重的状况下,若是数据填入后碰巧出现了功能性标识,好比定位标识的图样,还会干扰正常功能性标识的做用,致使QR码没法扫描。

举个栗子:

这样的数据产生的原始QR码明显含有大量大面积色块,扫描难度很高。

因此,掩码和以前提到的在数据后添加11101100和00010001交替的填充字节,都是为了不这种状况发生,让图像更“均匀”。

知道了掩码的重要性,咱们来看看掩码究竟是什么。在计算机科学中,掩码就是一个二进制串,经过和数据进行异或运算来变换数据。在QR码中,掩码也是经过异或运算来变换数据矩阵。因此你可能已经猜到了,QR码的掩码就是预先定义好的矩阵。QR标准经过生成规则定义了八个数据掩码:

  1. dark if (row + column) mod 2 == 0
  2. dark if (row) mod 2 == 0
  3. dark if (column) mod 3 == 0
  4. dark if (row + column) mod 3 == 0
  5. dark if ( floor(row / 2) + floor(column / 3) ) mod 2 == 0
  6. dark if ((row * column) mod 2) + ((row * column) mod 3) == 0
  7. dark if ( ((row * column) mod 2) + ((row * column) mod 3) ) mod 2 == 0
  8. dark if ( ((row + column) mod 2) + ((row * column) mod 3) ) mod 2 == 0

给定了规则咱们很容易写出代码来生成这些掩码:

可是且慢,你看出如今出现了什么问题吗?

对,掩码的范围也覆盖了功能性区域,要是用这样的掩码的话,功能性标识也难以幸免。因此咱们须要一个表明数据区域的“蒙版”来过滤掉功能性区域中的掩图案。这个“过滤”的过程能够经过矩阵间“与”运算来实现。

在 qrcode.py 开始添加矩阵间“与”运算函数和数据区域蒙版的填充:

def _matAnd(mat1, mat2): '''  Matrix-wise and.  Dark and dark -> dark  Light and light -> light  Dark and light -> light  Light and dark -> light  ''' res = [[_LIGHT for i in range(len(mat1[0]))] for j in range(len(mat1))] for j in range(len(mat1)): for i in range(len(mat1[0])): res[j][i] = int(mat1[j][i] == _LIGHT or mat2[j][i] == _LIGHT) return res # Data area mask to avoid applying masks to functional area. _dataAreaMask = [[_DARK for i in range(21)] for j in range(21)] _dataAreaMask = _matCp([[_LIGHT for i in range(9)] for j in range(9)], _dataAreaMask, 0, 0) _dataAreaMask = _matCp([[_LIGHT for i in range(9)] for j in range(8)], _dataAreaMask, 13, 0) _dataAreaMask = _matCp([[_LIGHT for i in range(8)] for j in range(9)], _dataAreaMask, 0, 13) _dataAreaMask = _matCp([[_LIGHT for i in range(4)]], _dataAreaMask, 6, 9) _dataAreaMask = _matCp([[_LIGHT] for i in range(4)], _dataAreaMask, 9, 6) 

填充出的数据区域蒙版效果如图

咱们在定义掩码时和蒙版进行“与”运算,就能够获得范围正确的掩码了。继续添加掩码定义

# Data masks defined in QR standard. _dataMasks = [] _dataMasks.append(_matAnd(_dataAreaMask, [[_DARK if (i+j)%2==0 else _LIGHT for i in range(21)] for j in range(21)])) _dataMasks.append(_matAnd(_dataAreaMask, [[_DARK if j%2==0 else _LIGHT for i in range(21)] for j in range(21)])) _dataMasks.append(_matAnd(_dataAreaMask, [[_DARK if i%3==0 else _LIGHT for i in range(21)] for j in range(21)])) _dataMasks.append(_matAnd(_dataAreaMask, [[_DARK if (i+j)%3==0 else _LIGHT for i in range(21)] for j in range(21)])) _dataMasks.append(_matAnd(_dataAreaMask, [[_DARK if (j/2 + i/3)%2==0 else _LIGHT for i in range(21)] for j in range(21)])) _dataMasks.append(_matAnd(_dataAreaMask, [[_DARK if (i*j)%2+(i*j)%3==0 else _LIGHT for i in range(21)] for j in range(21)])) _dataMasks.append(_matAnd(_dataAreaMask, [[_DARK if ((i*j)%2+(i*j)%3)%2==0 else _LIGHT for i in range(21)] for j in range(21)])) _dataMasks.append(_matAnd(_dataAreaMask, [[_DARK if ((i+j)%2+(i*j)%3)%2==0 else _LIGHT for i in range(21)] for j in range(21)])) 

效果如图

如今咱们就能够安心地使用这些掩码啦!

在 qrcode.py 开始添加矩阵间异或函数

def _matXor(mat1, mat2): '''  Matrix-wise xor.  Dark xor dark -> light  Light xor light -> light  Dark xor light -> dark  Light xor dark -> dark  ''' res = [[_LIGHT for i in range(len(mat1[0]))] for j in range(len(mat1))] for j in range(len(mat1)): for i in range(len(mat1[0])): res[j][i] = int(mat1[j][i] == mat2[j][i]) return res 

由于咱们用1来表示白色,0来表示黑色,因此异或和与的逻辑都是和正常逻辑相反的。

该实现 _mask 来给填了数据的QR码应用掩码了。但是不对啊,为何要八个掩码啊?这是由于考虑到数据的多样性,一种掩码难以达到预期的效果,因此QR标准定义了八个掩码,要求在应用掩码时先分别应用全部的掩码产生八个结果,而后根据惩罚规则计算出每一个结果矩阵的惩罚分,再选出惩罚分最小,效果最好的掩码当作最终结果。这一过程产生的掩码ID也是格式信息的一部分,来告诉扫描软件应该用哪一个掩码来还原数据。

QR标准把惩罚分分红了四项,分别对应行/列中的连续色条、大面积的色块、行/列中相似定位标识的部分、整个矩阵中颜色的不平衡作出加权惩罚。

(图片来源:ISO/IEC 18004: Information – Automatic identification and data capture techniques – QR Code barcode symbology specification

其中,N1=3,N2=3,N3=40,N4=10,i 是色条超出5的部分的长度。

在 _mask 以前添加 _penalty 的实现:

def _penalty(mat): '''  Calculate penalty score for a masked matrix.  N1: penalty for more than 5 consecutive pixels in row/column,  3 points for each occurrence of such pattern,  and extra 1 point for each pixel exceeding 5  consecutive pixels.  N2: penalty for blocks of pixels larger than 2x2.  3*(m-1)*(n-1) points for each block of mxn  (larger than 2x2).  N3: penalty for patterns similar to the finder pattern.  40 points for each occurrence of 1:1:3:1:1 ratio  (dark:light:dark:light:dark) pattern in row/column,  preceded of followed by 4 consecutive light pixels.  N4: penalty for unbalanced dark/light ratio.  10*k points where k is the rating of the deviation of  the proportion of dark pixels from 50% in steps of 5%.  ''' # Initialize. n1 = n2 = n3 = n4 = 0 # Calculate N1. for j in range(len(mat)): count = 1 adj = False for i in range(1, len(mat)): if mat[j][i] == mat[j][i-1]: count += 1 else: count = 1 adj = False if count >= 5: if not adj: adj = True n1 += 3 else: n1 += 1 for i in range(len(mat)): count = 1 adj = False for j in range(1, len(mat)): if mat[j][i] == mat[j-1][i]: count += 1 else: count = 1 adj = False if count >= 5: if not adj: adj = True n1 += 3 else: n1 += 1 # Calculate N2. m = n = 1 for j in range(1, len(mat)): for i in range(1, len(mat)): if mat[j][i] == mat[j-1][i] and mat[j][i] == mat[j][i-1] and mat[j][i] == mat[j-1][i-1]: if mat[j][i] == mat[j-1][i]: m += 1 if mat[j][i] == mat[j][i-1]: n += 1 else: n2 += 3 * (m-1) * (n-1) m = n = 1 # Calculate N3. count = 0 for row in mat: rowstr = ''.join(str(e) for e in row) occurrences = [] begin = 0 while rowstr.find('0100010', begin) != -1: begin = rowstr.find('0100010', begin) + 7 occurrences.append(begin) for begin in occurrences: if rowstr.count('00000100010', begin-4) != 0 or rowstr.count('01000100000', begin) != 0: count += 1 transposedMat = _transpose(mat) for row in transposedMat: rowstr = ''.join(str(e) for e in row) occurrences = [] begin = 0 while rowstr.find('0100010', begin) != -1: begin = rowstr.find('0100010', begin) + 7 occurrences.append(begin) for begin in occurrences: if rowstr.count('00000100010', begin-4) != 0 or rowstr.count('01000100000', begin) != 0: count += 1 n3 += 40 * count # Calculate N4. dark = sum(row.count(_DARK) for row in mat) percent = int((float(dark) / float(len(mat)**2)) * 100) pre = percent - percent % 5 nex = percent + 5 - percent % 5 n4 = min(abs(pre-50)/5, abs(nex-50)/5) * 10 return n1 + n2 + n3 + n4 

(插一句,我实现的这个 _penalty 尚未测试正确性。。)

(大概仔细看完了辣么一大段代码而后看到上一句的人会想来打我吧。。)

实现 _mask :

def _mask(mat): '''  Mask the data QR code matrix with all 8 masks,  call _penalty to calculate penalty scores for each  and select the best mask.  Return tuple(selected masked matrix, number of selected mask).  ''' maskeds = [_matXor(mat, dataMask) for dataMask in _dataMasks] penalty = [0] * 8 for i, masked in enumerate(maskeds): penalty[i] = _penalty(masked) # Find the id of the best mask. index = penalty.index(min(penalty)) return maskeds[index], index 

这里考虑到 _mask 是由 _fillInfo 调用,而填写格式信息须要选择的掩码的ID,咱们让 _mask 返回告终果矩阵和掩码ID构成的tuple。

用咱们以前的栗子测试一下掩码效果:

效果不错!

  • 填充格式信息

只剩最后一步了!格式信息很简单,由两位容错等级代码和三位QR掩码代码构成。

容错等级代码:

(图片来源:ISO/IEC 18004: Information – Automatic identification and data capture techniques – QR Code barcode symbology specification

QR掩码代码:

固然格式信息也是要加容错码的。格式信息的容错算法采用(15,5)BCH码


编码

构建码字为

(c14, c13, ..., c8)

这样多项式为

c14+c13+...+c8

咱们将它称为 CI。

而后就要找出 CR 知足 CR=CI (mod m1,3(x))=c7+c6+...+c0

这样就获得待发的码字 C(x) = CI+CR (mod m1,3(x)) = 0

例如,若是咱们要对 (1,1,0,0,1,1,0) 进行编码

CI=x14+x13+x10+x9

而后用 m1,3(x) 除以(这里的除法是多项式除法)CI ,获得结果为 CR(x),在Z2域中,咱们能够算出 CR为

x3+1

这样,待发的码字为

(1,1,0,0,1,1,0, 0,0,0,0,1,0,0,1)

(Wikipedia: BCH码,

.......................................................

咳咳。。去看Reed–Solomon codes for coders,都讲得很清楚嘛,很容易就看懂了对不对?(和蔼的微笑)

计算得出十位BCH容错码接在格式信息以后,还要与掩码101010000010010进行异或,做用同QR掩码。

在 _fillInfo 以前添加 _fmtEncode 实现容错码计算和应用掩码:

def _fmtEncode(fmt): '''Encode the 15-bit format code using BCH code.''' g = 0x537 code = fmt << 10 for i in range(4,-1,-1): if code & (1 << (i+10)): code ^= g << i return ((fmt << 10) ^ code) ^ 0b101010000010010 

(Source: Wikiversity: Reed–Solomon codes for coders

有了编码好的格式信息,就能够把它按照标准填入矩阵了。

(图片来源:ISO/IEC 18004: Information – Automatic identification and data capture techniques – QR Code barcode symbology specification

其中14表明最高位(most significant bit),0表明最低位(least significant bit)。

继续实现 _fillInfo:

def _fillInfo(arg): '''  Fill the encoded format code into the masked QR code matrix.  arg: (masked QR code matrix, mask number).  ''' mat, mask = arg # 01 is the format code for L error control level, # concatenated with mask id and passed into _fmtEncode # to get the 15 bits format code with EC bits. fmt = _fmtEncode(int('01'+'{:03b}'.format(mask), 2)) fmtarr = [[not int(c)] for c in '{:015b}'.format(fmt)] mat = _matCp(_transpose(fmtarr[7:]), mat, 8, 13) mat = _matCp(fmtarr[9:][::-1], mat, 0, 8) mat = _matCp(fmtarr[7:9][::-1], mat, 7, 8) mat = _matCp(fmtarr[:7][::-1], mat, 14, 8) mat = _matCp(_transpose(fmtarr[:6]), mat, 8, 0) mat = _matCp([fmtarr[6]], mat, 8, 7) return mat 

至此QR码所有完成(撒花花 ︿( ̄︶ ̄)︿)。

  • 接口

最后一步,为咱们的QR码生成器提供调用接口:

def qrcode(data, width=210, filename='qrcode.jpg'): '''Module public interface''' try: _genImage(_genBitmap(_encode(data)), width, filename) except Exception, e: print e raise e 

哒哒哒哒!完成!(完整版代码已上传 GitHub:

别忘了咱们最初的目的:hello world!来试验一下吧!

Hello world! (二维码自动识别)

能!扫!描!了!

满满的成就感有没有!!!

但是忽然想到!!!

我只是想说一句 hello world啊!!!!!

那何很少说几句啊!!



  • 写在后面


凌晨2点14,终于完稿。我只有几点想说的

    1. 学习真有趣
    2. Python真好用
    3. 制定标准真是凝结了工程师的无限智慧
    4. 熬夜伤身
    5. 熬夜会饿
    6. 半夜饿真难受
    7. 第一次写东西,啰啰嗦嗦拖了这么长的篇幅
    8. 看到这里的都是真爱

(END)

Reference:

Wikipedia: QR code,

Wikipedia: QR码,

Wikipedia: Reed–Solomon error correction,

Wikipedia: 里德-所罗门码,

Wikipedia: BCH code,

Wikipedia: BCH码,

Wikiversity: Reed–Solomon codes for coders,

Thonky: QR Code Tutorial,

Python Imaging Library Handbook,

PIL-廖雪峰的官方网站,

ISO/IEC 18004: Information – Automatic identification and data capture techniques – QR Code barcode symbology specification