二維碼詳解(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

相關文章
相關標籤/搜索