二維碼(QR Code)的生成原理及解析
自從大街小巷的小商小販都開始佈滿了騰訊爸爸和阿里爸爸的二維碼之後,我才感覺到我大天朝共享支付的優越性。最近畢業論文寫的差不多了,在入職之前多學一些東西也是好的。這裡秉著好奇心,研究一下二維碼的生成,並嘗試性寫一個二維碼解析原始碼。
注:暫時只有二維碼原理,筆者這段時間會持續研究解析程式碼,並隨進度持續更新。
一. 二維碼基本知識
二維碼另一個名稱是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×rc=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 的基礎~
原始碼及解析筆者會隨學習的進度持續更新~
八. 後記
筆者學習完畢二維碼內容後不禁感嘆,二維碼規則的制定當真是凝聚了多少研究者的心血。學無止境,在知識的海洋中,當真是需要抱著敬畏之心和謙卑的態度,才能體會到這片海洋的浩瀚。
研究二維碼的過程十分有趣,學到了不少東西,後續過程中筆者會持續更新對二維碼的學習心得體會~