二維碼生成原理及解析代碼
自從大街小巷的小商小販都開始佈滿了騰訊爸爸和阿里爸爸的二維碼之後,我才感覺到我大天朝共享支付的優越性。最近畢業論文寫的差不多了,在入職之前多學一些東西也是好的。這裏秉着好奇心,研究一下二維碼的生成,並嘗試性寫一個二維碼解析源碼。
注:暫時只有二維碼原理,筆者這段時間會持續研究解析代碼,並隨進度持續更新。
參考網址:
《二維碼的生成細節和原理》
《QR Code Tutorial》
《Hello World!》—— 知乎專欄文章
《爲程序員寫的Reed-Solomon碼解釋》
一. 二維碼基本知識
二維碼另一個名稱是QR Code(Quick Response Code),近年來在移動設備上經常使用,與傳統條形碼相比,可以存儲更多的信息。二維碼本質上是個密碼算法,基本知識總結如下。
首先,二維碼存在 40 種尺寸,在官方文檔中,尺寸又被命名爲 Version。尺寸與 Version 存在線性關係:Version 1 是 21×21 的矩陣,Version 2 是 25×25 的矩陣,每增加一個 Version,尺寸都會增加 4,故尺寸 Size 與 Version 的線性關係爲:
Version 的最大值是 40,故尺寸最大值是(40-1)*4+21 = 177,即 177 x 177 的矩陣。
二維碼結構如下圖 1.1 所示:
圖1.1 二維碼結構
二維碼的各部分都有自己的作用,基本上可被分爲定位、功能數據、數據內容三部分。
- 定位圖案:
- Position Detection Pattern, 定位圖案:用於標記二維碼矩形的大小;用三個定位圖案即可標識並確定一個二維碼矩形的位置和方向了;
- Separators for Position Detection Patterns, 定位圖案分割器:用白邊框將定位圖案與其他區域區分;
- Timing Patterns, 時序圖案:用於定位,二維碼如果尺寸過大,掃描時容易畸變,時序圖案的作用就是防止掃描時畸變的產生;
- Alignment Patterns, 對齊圖案:只有在 Version 2 及其以上纔會需要;
- 功能數據:
- Format Information, 格式信息:存在於所有尺寸中,存放格式化數據;
- Version Information, 版本信息:用於 Version 7 以上,需要預留兩塊 3×6 的區域存放部分版本信息;
- 數據內容:剩餘部分存儲數據內容
- Data Code, 數據碼;
- Error Correction Code, 糾錯碼;
二. 數據編碼
2.1 數據編碼信息
二維碼的數據編碼信息如下圖 2.1, 2.2 中的列表所示:
圖2.1 模式編號指示器
圖2.2 字符計數指示器中的位數
上圖 2.1 中,展示的是二維碼支持的數據編碼模式。
注:其中中文編碼模式爲 1101;
上圖 2.2 中展示了不同版本(即不同尺寸)的二維碼,單個編碼對應二進制的位數。
注:二維碼規格說明書中,存在各式各樣的編碼規範表;
圖2.1, 2.2 表格具體含義,在後面的例程中會具體講解。
2.2 數據編碼形式
2.2.1 數字編碼(Numeric Mode)
數字編碼的範圍爲 0~9。
對於數字編碼,統計需要編碼數字的個數是否爲 3 的倍數:如果不是 3 的倍數,則剩下的 1 位或 2 位會被轉爲 4bits 或 8bits(十進制轉二進制),每三位數字都會被編成 10bits, 12bits, 14bits,具體編碼長度仍然需要二維碼尺寸決定。
2.2.2 字符編碼(Alphanumeric Mode)
字符編碼的範圍有:
- 數字 0~9;
- 大寫 A~Z(無小寫);
- 幾個符號$ % * + - . / 和空格。
上述字符映射爲一個索引表,如下圖 2.3 所示:
圖2.3 字符映射索引表
圖中 Char 表示字符,Value 表示字符對應的索引值。
索引表中共 45 種對應關係,字符編碼的過程,就是將每兩個字符分爲一組,然後轉成上圖 2.3 的 45 進制,再轉爲 11bits 的二進制結果。對於落單的一個字符,則轉爲 6bits 的二進制結果。
此外,根據上圖 2.2 的設定,對不同 Version 的二維碼使用 9/11/13 個二進制表示。
注:
上圖 2.3 中的 SP 代表空格。
2.2.3 字節編碼(Byte Mode)
可以是 0-255 的 ISO-8859-1 字符。有些二維碼的掃描器可以自動檢測是否是 UTF-8 的編碼。
2.2.4 日文編碼(Kanji Mode)
日文編碼同時也是雙字節編碼,同樣也可以用於中文編碼。
日文與中文編碼流程基本相似:
- 首先減去一個值;
- 挑出差值結果的前兩個 16 進制,乘以 0xC0;
- 加上後兩個 16 進制位;
- 轉爲 13bits 編碼;
按照日文編碼集 SHIFT_JIS爲參照,可查詢日文字符的對應編碼。以“雅”與“芒”爲例,轉換過程如下圖 2.4 所示:
圖2.4 日文編碼流程展示
2.2.5 其他編碼
其他類型的編碼本文中不詳細說明。其中包括:
- 特殊字符集(Extended Channel Interpretation Mode):主要用於特殊的字符集,並不是所有的掃描器都支持這種編碼;
- 混合編碼(Structured Append Mode):說明該二維碼中包含了多種編碼格式;
- 特殊行業編碼(FNC1 Mode):主要是給一些特殊的工業或行業用的,如GS1條形碼等;
2.3 數據編碼示例說明
分別用一個數字編碼與字符編碼的示例,說明數據編碼的過程:
2.3.1 例程1:數字編碼
問題:對於 Version 1 尺寸的二維碼,糾錯級別爲 H,編碼爲:01234567
解析步驟:
- 將上述數字分爲三組:012, 345, 67;
- 查詢圖 2.2 表格內容,Version 1 二維碼的數字編碼應轉換爲 10bits 的二進制數字,故將上面三組數字轉爲二進制分別爲:012→0000001100, 345→0101011001, 67→1000011;
- 將三個二進制串連接起來:0000001100 0101011001 1000011;
- 將數字的個數轉成二進制:對於數字編碼,數字長度依舊用圖 2.2 表格中查到的 10bits 二進制數字來表示,數字共有 8 個,故數字個數的二進制形式爲:8→0000001000;
- 查詢圖 2.1 表格內容,數字編碼的標誌爲 0001,將編碼標誌與步驟 4 編碼結果加到步驟 3 結果之前,故最終結果爲:0001 0000001000 0000001100 0101011001 1000011
2.3.2 例程2:字符編碼
問題:對於 Version 1 尺寸的二維碼,糾錯級別爲 H,編碼爲:AE-86
解析步驟:
- 在圖 2.3 的字符索引表中分別找到 AE-86 五個字符的索引分別爲:(10, 14, 41, 8, 6);
- 將五個字符兩兩分組:(10, 14) (41, 8) (6);
- 字符編碼應將字符組轉換爲 11bits 的二進制,故上述三組字符首先轉爲 45 進制後再轉爲二進制:
- (10, 14):轉爲 45 進制:10×45+14=464;再轉爲 11bits 的二進制:00111010000;
- (41, 8):轉爲 45 進制:41×45+8=1853;再轉爲 11bits 的二進制:11100111101;
- (6):轉爲 45 進制:6;再轉爲 6bits 的二進制:000110;
- 將步驟 3 中得到的三個二進制結果連接起來:00111010000 11100111101 000110;
- 查詢圖 2.2 表格內容,Version 1 二維碼的字符個數應轉換爲 9bits 的二進制數字,對於 5 個字符,二維碼字符個數轉爲 9bits 二進制爲:000000101;
- 查詢圖 2.1 表格內容,字符編碼的標誌爲 0010,將編碼標誌與步驟 5 編碼結果加到步驟 4 結果之前,故最終編碼結果爲:0010 000000101 00111010000 11100111101 000110;
三. 結束符與補齊符
對於結束符和補齊符,我們直接舉例進行說明。
問題:對於 Version 1 尺寸的二維碼,糾錯級別爲 H,以筆者的英文名作爲編碼:CHANDLERGENG
按照 2.3.2 字符編碼例程進行分析,得到編碼如下:
編碼 | 字符數 | CHANDLERGENG 的編碼 |
---|---|---|
0010 | 000001101 | 01000101101 00111011001 01001011110 01010010001 01011011110 10000011011 |
3.1 結束符
在需要在對於上述字符的編碼,需要在最後加上結束符。結束符爲連續 4 個 0 值。加上結束符後,得到的編碼如下:
編碼 | 字符數 | CHANDLERGENG 的編碼 | 結束 |
---|---|---|---|
0010 | 000001101 | 01000101101 00111011001 01001011110 01010010001 01011011110 10000011011 | 0000 |
如果所有的編碼加起來不是 8 的倍數,則還需要在後面加上足夠的 0。如上面一共有 83bits,所以與 8 的倍數還相差兩位,故在最後加上 5 個 0,上表最終的數據變爲:
00100000 01101010 00101101 00111011 00101001 01111001 01001000 10101101 11101000 00110110 00000000
3.2 補齊符
如果最後還沒有達到我們最大的 Bits 數限制,則需要在編碼最後加上補齊符(Padding Bytes)。
補齊符內容是不停重複兩個字節:11101100 和 00010001。這兩個二進制轉成十進制,分別爲 236 與17,具體不知道爲什麼選這兩個值……關於每一個Version的每一種糾錯級別的最大Bits限制,可以參看 QR Code Spec 的第35頁到44頁的 Table-7 一表(筆者參考的是《ISO/IEC 18004》2000版),大致如下圖 3.1 所示:
圖3.1 二維碼糾錯級別的最大Bits限制(部分)
上圖 3.1 中提到的 codewords,可譯爲碼字,一個碼字是一個字節。對於 Version 1 的 H 糾錯級別,共需要 26 個碼字,即 104bits。現在加上用 0 補全的結束符,已經有了 88bits,故還需要補上 16 bits。補齊後的編碼爲:
00100000 01101010 00101101 00111011 00101001 01111001 01001000 10101101 11101000 00110110 00000000 11101100 00010001
以上數據即爲數據碼(Data Codewords)。
四. 糾錯碼
前文提到了不同的糾錯級別(Error Correction Code Level)。有了糾錯機制,纔可以使得有些二維碼有了殘缺也可以掃碼解析出來,纔可以使得二維碼中心位置可以供某些商家加上對解析不必要的圖標。
二維碼一共有四種糾錯級別:
糾錯水平 | 可被修正容量 |
---|---|
L | 7% 碼字 |
M | 15% 碼字 |
Q | 25% 碼字 |
H | 30% 碼字 |
二維碼對數據碼加上糾錯碼的過程,首先要對數據碼進行分組,即分成不同的塊(Block)。參看如上圖 3.1 所示 QR Code Spec 的第35頁到44頁的 Table-7 中的最下方說明了分組的定義表:
圖4.1 二維碼糾錯級別說明(部分)
對於表中的最後兩列的內容:
- 糾錯塊個數(Number of error correction blocks):需要劃分糾錯快的個數;
- 糾錯塊碼字數(Error Correction Code Per Blocks):每個塊中的碼字個數,即有多少個字節Bytes;
表中最下面關於 (c,k,r) 的解釋:
- c:碼字總個數;
- k:數據碼個數;
- r:糾錯碼容量
注:
- c,k,r的關係公式:
c=k+2×r 。 - 糾錯碼容量小於糾錯碼個數的一般
以上圖 4.1 中的 Version 5 + H 糾錯機爲例:圖中紅色方框說明共需要 4 個塊(上下行各一組,每組 2 個塊)。
第一組的屬性:
- 糾錯塊個數 = 2:該組中有兩個塊;
- (c, k, r) = (33, 11, 11):該組中每個塊共有 33 個碼字,其中 11 個數據碼, 11×2=22 個糾錯碼;
第二組的屬性:
- 糾錯塊個數 = 2:該組中有兩個塊;
- (c, k, r) = (34, 12, 11):該組中每個塊共有 34 個碼字,其中 12 個數據碼, 11×2=22 個糾錯碼;
具體示例如下表所示,且由於使用二進制會使得表格過大,故轉爲範圍在 0~255 的十進制。其中組 1 的每個塊,都有 11 個數據碼, 22 個糾錯碼;組 2 的每個塊,都有 12 個數據碼,22 個糾錯碼。
組 | 塊 | 數據 | 每個塊的糾錯碼 |
---|---|---|---|
1 | 1 2 |
67 85 70 134 87 38 85 194 119 50 6 66 7 118 134 242 7 38 86 22 198 199 |
199 11 45 115 247 241 223 229 248 154 117 236 38 6 50 17 7 236 213 87 148 235 177 212 76 133 75 242 238 76 195 230 189 106 248 134 76 40 154 27 195 255 117 129 |
2 | 1 2 |
247 119 50 7 118 134 87 38 82 6 134 151 194 6 151 50 16 236 17 236 17 236 17 236 |
96 60 202 182 124 157 200 134 27 129 209 182 70 85 246 230 247 70 66 247 118 134 173 24 147 59 33 106 40 255 172 82 2 157 242 33 229 200 238 106 248 134 76 40 |
二維碼的糾錯碼主要是通過裏德-所羅門糾錯算法(Reed-Solomon Error Correction)實現的。
(關於 Reed-Solomon 算法,現在此處佔坑,回頭研究了再寫上去)
五. 最終編碼
此時得到了數據,但還不能開始畫圖,因爲二維碼還需要將數據碼與糾錯碼的各個字節交替放置。
5.1 穿插放置
繼續以第四章中給出的示例爲例,給出其穿插放置的過程。
5.1.1 數據碼穿插放置
第四章示例中的數據碼如下表所示:
塊數 | |||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
塊1 | 67 | 85 | 70 | 134 | 87 | 38 | 85 | 194 | 119 | 50 | 6 |
塊2 | 66 | 7 | 118 | 134 | 242 | 7 | 38 | 86 | 22 | 198 | 199 |
塊3 | 247 | 119 | 50 | 7 | 118 | 134 | 87 | 38 | 82 | 6 | 134 |
塊4 | 194 | 6 | 151 | 50 | 16 | 236 | 17 | 236 | 17 | 236 | 17 |
提取每一列數據:
- 第一列:67, 66, 247, 194;
- 第二列:85, 7, 119, 6;
- ……
- 第十一列:6, 199, 134, 17;
- 第十二列:151, 236;
將上述十二列的數據拼在一起:67, 66, 247, 194, 85, 7, 119, 6,…, 6, 199, 134, 17, 151, 236。
糾錯碼如下表所示:
塊數 | |||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
塊1 | 199 | 11 | 45 | 115 | 247 | 241 | 223 | 229 | 248 | 154 | 117 |
塊2 | 177 | 212 | 76 | 133 | 75 | 242 | 238 | 76 | 195 | 230 | 189 |
塊3 | 96 | 60 | 202 | 182 | 124 | 157 | 200 | 134 | 27 | 129 | 209 |
塊4 | 173 | 24 | 147 | 59 | 33 | 106 | 40 | 255 | 172 | 82 | 2 |
同樣的方法,將 22 列數據放在一起:199, 177, 96, 173, 11, 212, 60, 24, …, 148, 117, 118, 76, 235, 129, 134, 40。
上述部分即爲二維碼的數據區。
5.2 剩餘位 (Remainder Bits)
對於某些 Version 的二維碼,得到上面的數據區結果長度依舊不足,需要加上最後的剩餘位。比如對於 Version 5 + H 糾錯等級的二維碼,剩餘位需要加 7bits,即加 7 個 0。參看 QR Code Spec 的 Table-1 一表即可查詢不同 Version 的剩餘位信息,如下圖 5.1 所示:
圖5.1 不同 Version 的剩餘位
六. 二維碼的繪製
終於講到二維碼繪製過程了,繪製的過程按照順序對圖 1.1 中各個重要部分依次講解。
6.1 定位圖案 (Position Detection Pattern)
首先在二維碼的三個角上繪製定位圖案。定位圖案與尺寸大小無關,一定是一個 7×7 的矩陣。如下圖 6.1 所示:
圖6.1 定位圖案 (Position Detection Pattern)
6.2 對齊圖案 (Alignment Pattern)
然後繪製對齊圖案。對齊圖案與尺寸大小無關,一定是一個 5×5 的矩陣。如下圖 6.2 所示:
圖6.2 對齊圖案 (Alignment Pattern)
對齊圖案繪製的位置,可參看 QR Code Spec 的 Table-E.1 一表查詢,部分內容如下圖 6.3 所示:
圖6.3 對齊圖案位置索引表(部分)
下圖 6.4 是上述表格中 Version 8 的一個例子,對於 Version 8 的二維碼,行列值在 6, 24, 42 的幾個點都會有對齊圖案。
圖6.4 對齊圖案例程 1
下圖 6.5 是最近我老媽慫恿我用支付寶搶紅包時給我發來的二維碼,該二維碼中只有一個對齊圖案, 故 Version 應在 V2——V6 之間。
圖6.5 對齊圖案例程 2
6.3 時序圖案 (Timing Pattern)
時序圖案是兩條連接三個定位圖案的線,如下圖 6.6 所示:
圖6.6 時序圖案例程 1
依舊拿支付寶紅包的二維碼爲例,其時序圖案如圖 6.7 所示:
圖6.7 時序圖案例程 2
6.4 格式信息
格式信息如下圖 6.8 所示:
圖6.8 格式信息
格式信息在定位圖案周圍分佈,由於定位圖案個數固定爲 3 個,且大小固定,故格式信息也是一個固定 15bits 的信息。每個 bit 的位置如下圖 6.9 所示:(注:圖中的 Dark Module 是固定永遠出現的)
圖6.9 格式信息位置
15bits 中數據,按照 5bits 的數據位 + 10bits 糾錯位的順序排列:
- 數據位佔 5bits:其中 2bits 用於表示使用的糾錯等級 (Error Correction Level),3bits 用於表示使用的蒙版 (Mask) 類別;
- 糾錯位佔 10bits:主要通過 BCH Code 計算;
爲了減少掃描後圖像識別的困難,最後還需要將 15bits 與 101010000010010 做異或 XOR 操作。因爲我們在原格式信息中可能存在太多的 0 值(如糾錯級別爲 00,蒙版 Mask 爲 000),使得格式信息全部爲白色,這將增加分析圖像的困難。
糾錯等級的編碼如下圖 6.10 的表格所示:
圖6.10 糾錯等級編碼
關於蒙版圖案的生成,在後文 6.7 中具體說明。格式信息的示例如下:
假設存在糾錯等級爲 M(對應 00),蒙版圖案對應 000,5bits 的數據位爲 00101,10bits 的糾錯位爲 0011011100:
則生成了在異或操作之前的 bits 序列爲:001010011011100
與 101010000010010 做異或 XOR 操作,即得到最終格式信息:100000011001110
6.5 版本信息 (Version Information)
對於 Version 7 及其以上的二維碼,需要加入版本信息。如下圖 6.11 藍色部分所示:
圖6.11 版本信息
版本信息依附在定位圖案周圍,故大小固定爲 18bits。水平豎直方向的填充方式如下圖 6.12 所示:
圖6.12 版本信息填充方式
18bits 的版本信息中,前 6bits 爲版本號 (Version Number),後 12bits 爲糾錯碼 (BCH Bits)。示例如下:
假設存在一個 Version 爲 7 的二維碼(對應 6bits 版本號爲 000111),其糾錯碼爲 110010010100;
則版本信息圖案中的應填充的數據爲:000111110010010100
6.6 數據碼與糾錯碼
此後即可填充第五章得到的數據內容了。填充的思想如下圖 6.13 的 Version 3 二維碼所示,從二維碼的右下角開始,沿着紅線進行填充,遇到非數據區域,則繞開或跳過。
圖6.13 二維碼數據填充(原始版)
然而這樣難以理解,我們可以將其分爲許多小模塊,然後將許多小模塊串連在一起,如下圖 6.14 所示(截取自 QR Code Spec 的圖 15):
圖6.14 二維碼數據填充
小模塊可以分爲常規模塊和非常規模塊,每個模塊的容量都爲 8。常規情況下,小模塊都爲寬度爲 2 的豎直小矩陣,按照方向將 8bits 的碼字填充在內。非常規情況下,模塊會產生變形。
填充方式上圖 6.14,圖中深色區域(如 D1 區域)填充數據碼,白色區域(如 E15 區域)填充糾錯碼。遍歷順序依舊從最右下角的 D1 區域開始,按照蛇形方向(D1→D2→…→D28→E1→E2→…→E16→剩餘碼)進行小模塊的填充,並從右向左交替着上下移動。下面給出若干填充原則:
原則 1:無論數據的填充方向是向上還是向下,常規模塊(即 8bits 數據全在兩列內)的排列順序應是從右向左,如下圖 6.15所示;
圖6.15 常規模塊內的填充方向
原則 2:每個碼字的最高有效位(即第7個bit)應置於第一個可用位。對於向上填充的方向,最高有效位應該佔據模塊的右下角;向下填充的方向,最高有效位佔據模塊的右上方。
注:對於某些模塊(以下圖 6.17 爲例),如果前一個模塊在右邊模塊的列內部結束,則該模塊成爲不規則模塊,且與常規模塊相比,原本填充方向向上時,最高位應該在右上角,此時則變爲左下角;
原則 3:當一個模塊的兩列同時遇到對齊圖案或時序圖案的水平邊界時,它將繼續在圖案的上方或下方延續;
原則 4:當模塊到達區域的上下邊界(包括二維碼的上下邊界、格式信息、版本信息或分隔符)時,碼字中任何剩餘 bits 將填充在左邊的下一列中,且填充方向反轉;如下圖 6.16 中的兩個模塊遇到了二維碼的上邊界,則方向發生變化;
圖6.16 非常規模塊填充方向的改變(舉例於 QR Code Spec 圖 13)
原則 5:當模塊的右一列遇到對齊圖案,或遇到被版本信息佔據的區域時,數據位會沿着對齊圖案或版本信息旁邊的一列繼續填充,並形成一個不規則模塊。如果當前模塊填充結束之前,下一個的兩列都可用,則下一個碼字的最高有效位應該放在單列中,如下圖 6.17 所示:
圖6.17 模塊單列填充
6.7 蒙版圖案
按照上述思路即可將二維碼填充完畢。但是那些點並不均衡,如果出現了大面積的空白或黑塊,掃描識別會十分困難,所以按照在前文 6.4 中格式信息的處理思路,對整個圖像與蒙版進行蒙版操作(Masking),蒙版操作即爲異或 XOR 操作。
二維碼又 8 種蒙版可以使用,如下圖 6.18 所示,公式也在圖中說明。蒙版只會和數據區進行異或操作,不會影響與格式信息相關的功能區。
注:選擇一個合適的蒙版也是有一定算法的。
蒙版圖案如下圖 6.18 所示,對應的產生公式與蒙版 ID 如下圖 6.19 的表格所示:
圖6.18 蒙版圖案
圖6.19 蒙版圖案產生公式
蒙版操作的過程與對比圖如下圖 6.20 所示,圖中最上層是沒有經過蒙版操作的原始二維碼,其中存在大量黑色區域,難以後續的分析識別。經過兩種不同蒙版的處理,可以看到最後生成的二維碼變的更加混亂,容易識別。
圖6.20 蒙版操作示例
蒙版操作之後,得到的二維碼即爲最終我們平常看到的結果。
七. 源碼
筆者原本準備用 C++ 與 OpenCV 寫一個二維碼解析程序,現在學了二維碼的原理後,發現好難。另外網上關於二維碼解析與生成的程序基本都是用 Python 寫的,筆者又想找個合適機會學習一下 Python,所以這段時間就準備從二維碼入手,學習一下 Python 的基礎~
源碼及解析筆者會隨學習的進度持續更新~
八. 後記
筆者學習完畢二維碼內容後不禁感嘆,二維碼規則的制定當真是凝聚了多少研究者的心血。學無止境,在知識的海洋中,當真是需要抱着敬畏之心和謙卑的態度,才能體會到這片海洋的浩瀚。
研究二維碼的過程十分有趣,學到了不少東西,後續過程中筆者會持續更新對二維碼的學習心得體會~