# 怎樣用Python識別條形碼?[譯]
現(xiàn)在每個人都在使用條形碼,大家卻幾乎注意不到。當我們在商店買東西時,貨品的識別使用條形碼。倉庫中的貨物,郵政包裹等也同樣使用條形碼來識別。但實際上并沒有多少人知道條形碼是如何工作的。
條形碼包含了什么內容,這個圖像的編碼內容是什么?

讓我們來弄清楚,并寫出我們自己的解碼器。
## 介紹
使用條形碼已經(jīng)有很長的歷史。首次嘗試實現(xiàn)自動化是在50年代完成的,當時一個讀碼系統(tǒng)被授予專利。在賓夕法尼亞鐵路公司工作的大衛(wèi).柯林斯(David Collins)決定簡化火車車廂分揀過程。他的方法是 —— 用不同的顏色條紋來編制車廂標識碼,然后使用光電管讀取它們。1962年這套編碼成為美國鐵路協(xié)會的標準(即KarTrak系統(tǒng))。到了1968年,為了增加識別準確率,同時減小讀碼器的尺寸,采用激光替代了氙氣燈。1973年開發(fā)出通用產(chǎn)品編碼(UPC碼),1974年第一個帶條碼的百貨商品(箭牌口香糖)開始在美國銷售。1984年條形碼已經(jīng)在全美商店使用,其他國家稍后也開始流行。
對于不同的應用,有不同的條碼類型。比如字符串“12345678”可以被編碼成下列這些條碼(不是全部喲):

讓我們開始分析。為了方便理解其原理,下面所有條形碼均使用 Code-128 碼。若想嘗試其他編碼,請使用 [在線條碼生成器](https://barcode.tec-it.com/en/Code128) 自行處理。
初看條形碼象一組隨機的數(shù)字,實際上它的結構井井有條:

1 — 空白區(qū),需要確定條碼的起始位置。
2 — 開始位 。有三種Code-128類型可供選擇(叫作A,B和C)。開始位相應分別是11010000100, 11010010000 或 11010011100 。不同類型的編碼表是不同的(詳見Code_128規(guī)范)
3 — 條碼本身,包含用戶數(shù)據(jù)。
4 — 校驗位。
5 — 停止位。對于 Code-128是 1100011101011 。
6(1) — 空白區(qū)。
現(xiàn)在讓我們來看看這些位是如何編碼的。其實很簡單——如果我們將最細的線寬設為 ?1?,那么2倍的線寬就是?11?,3倍的線寬就是 ?111?,以此類推。空白寬度按照同樣原則,分別代表 ?0?, ?00? 或 ?000?。有興趣的人可以比較上面圖片驗證規(guī)則是否有效。
現(xiàn)在我們可以開始編碼了。
## 獲得條碼序列
一般來說,這是最復雜的部分,可以通過不同的方式實現(xiàn)。 我不確定我的方法是否是最優(yōu)的,但對于我們的任務來說,這絕對是足夠的。
首先,讓我們加載圖像,拉伸其寬度,從中間裁剪一條水平線,將其轉換為黑白顏色并保存到數(shù)組中。
```
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
image_path = "barcode.jpg"
img = Image.open(image_path)
width, height = img.size
basewidth = 4*width
img = img.resize((basewidth, height), Image.ANTIALIAS)
hor_line_bw = img.crop((0, int(height/2), basewidth, int(height/2) + 1)).convert('L')
hor_data = np.asarray(hor_line_bw, dtype="int32")[0]
```
在條形碼中黑線對應?1?,但是在RGB中正相反,黑色對應?0?,所以數(shù)組中數(shù)據(jù)值需要倒置。另外我們還需要計算數(shù)組的平均值。
```
hor_data = 255 - hor_data
avg = np.average(hor_data)
plt.plot(hor_data)
plt.show()
```
讓我們運行程序來驗證條形碼被正確加載:

現(xiàn)在我們需要確定一個數(shù)位的寬度。為此我們要提取數(shù)據(jù),記錄黑白線分界點的位置
```
pos1, pos2 = -1, -1
bits = ""
for p in range(basewidth - 2):
? ? if hor_data[p] < avg and hor_data[p + 1] > avg:
? ? ? ? bits += "1"
? ? ? ? if pos1 == -1:
? ? ? ? ? ? pos1 = p
? ? ? ? if bits == "101":
? ? ? ? ? ? pos2 = p
? ? ? ? ? ? break
? ? if hor_data[p] > avg and hor_data[p + 1] < avg:
? ? ? ? bits += "0"
bit_width = int((pos2 - pos1)/3)
```
我們只記錄黑白線分界點的位置,所以條碼?1101?會被存為 ?101?,但是對于獲取條碼數(shù)位的像素寬度足夠了。
現(xiàn)在讓我們對數(shù)據(jù)進行解碼。我們需要找到每個條碼線,并找出其間距對應的位數(shù)。位數(shù)并不能精確匹配(條碼會被拉伸或扭曲一點),所以我們需要將結果四舍五入為整數(shù)值。
```
bits = ""
for p in range(basewidth - 2):
? ? if hor_data[p] > avg and hor_data[p + 1] < avg:
? ? ? ? interval = p - pos1
? ? ? ? cnt = interval/bit_width
? ? ? ? bits += "1"*int(round(cnt))
? ? ? ? pos1 = p
? ? if hor_data[p] < avg and hor_data[p + 1] > avg:
? ? ? ? interval = p - pos1
? ? ? ? cnt = interval/bit_width
? ? ? ? bits += "0"*int(round(cnt))
? ? ? ? pos1 = p
```
也許有更好的方法來做到這一點,大家可以寫到評論區(qū)。
如果一切都做得很完美,我們會得到類似的序列:
```
11010010000110001010001000110100010001101110100011011101000111011011
01100110011000101000101000110001000101100011000101110110011011001111
00010101100011101011
```
## 解碼
一般來說,解碼很容易。Code-128碼是11位條碼,具有不同的編碼類型(根據(jù)編碼類型—A,B或C,可以表示字母或[00]-[99]的數(shù)字對集合。
在我們的例子中,起始位是 11010010000,對應編碼類型B。我懶得手動輸入所有代碼,所以直接從維基百科頁面上復制粘貼它。解析每行的內容也是使用Python(提示—開發(fā)產(chǎn)品可別這么干)
```
? ? CODE128_CHART = """
? ? ? ? 0 _ _ 00 32 S 11011001100 212222
? ? ? ? 1 ! ! 01 33 ! 11001101100 222122
? ? ? ? 2 " " 02 34 " 11001100110 222221
? ? ? ? 3 # # 03 35 # 10010011000 121223
? ? ? ? ...
? ? ? ? 93 GS } 93 125 } 10100011110 111341
? ? ? ? 94 RS ~ 94 126 ~ 10001011110 131141
? ? ? ? 103 Start Start A 208 SCA 11010000100 211412
? ? ? ? 104 Start Start B 209 SCB 11010010000 211214
? ? ? ? 105 Start Start C 210 SCC 11010011100 211232
? ? ? ? 106 Stop Stop - - - 11000111010 233111""".split()
? ? SYMBOLS = [value for value in CODE128_CHART[6::8]]
? ? VALUESB = [value for value in CODE128_CHART[2::8]]
? ? CODE128B = dict(zip(SYMBOLS, VALUESB))
```
最后的部分很簡單。首先,把序列拆分成11位數(shù)據(jù)塊:
```
sym_len = 11
symbols = [bits[i:i+sym_len] for i in range(0, len(bits), sym_len)]
```
最后,生成字符串并顯示:
```
str_out = ""
for sym in symbols:
? ? if CODE128B[sym] == 'Start':
? ? ? ? continue
? ? if CODE128B[sym] == 'Stop':
? ? ? ? break
? ? str_out += CODE128B[sym]
? ? print("? ", sym, CODE128B[sym])
print("Str:", str_out)
```
我沒有在此顯示本文開頭條碼圖片的解碼結果,把它作為讀者的作業(yè)吧(使用下載的智能手機APP識別將被視為作弊:)
CRC校驗也沒有在此代碼中實現(xiàn),如有需要請自行解決。
當然,本算法并不完美,它只花了一個半小時完成。對于專業(yè)性任務可以使用現(xiàn)成的類庫,比如 pyzbar。其解碼條碼圖片,只需4行代碼足矣:
```
from pyzbar.pyzbar import decode
img = Image.open(image_path)
decode = decode(img)
print(decode)
```
(首先使用命令行 ?pip install pyzbar?安裝類庫)
**附:**關于條碼校驗位的算法歷史,讀者 vinograd19 寫了很有趣的評論
校驗位的計算很有趣。
校驗位很明顯是為了避免解碼錯誤。如果一個代碼是1234,被解碼為7234,我們需要一個方法拒絕1變成7。驗證方法可以不完美,但是至少90%條碼能夠被正確驗證。
第一步算法:讓我們得到數(shù)字和,且余數(shù)為0.第一個符號包含數(shù)據(jù),最后一個數(shù)字是這樣選擇的,數(shù)字和除以10。解碼后,如果數(shù)字和不能被10整除,則解碼錯誤,需要重新解碼。比如,條碼1234有效—1+2+3+4 = 10。條碼1216也有效,但1218無效。
這避免了解碼問題。但是條碼可以通過硬件鍵盤手工輸入。這個方法的另一個缺陷被發(fā)現(xiàn)——如果訂單的兩位數(shù)字被交換了,校驗位仍然正確,這太糟了。不如,代碼1234被輸入為2134,校驗位仍然是一樣的。如果人們輸入數(shù)字很快,錯誤的數(shù)字順序是常見的情況。
第二步算法:改進校驗位算法——計算奇數(shù)位兩次。這樣,如果訂單號改變了,數(shù)字和就不對了。比如代碼2364是有效的(2 + 3\*2+ 6 + 4\*2 = 22),但代碼3264是無效的(3 + 2\*2 + 6 + 4\*2 = 21)。很好,但是另一種情況又出現(xiàn)了。有些鍵盤是兩行10鍵,第一行是12345,第二行是67890.如果 ?1? 輸入成?2?,檢驗碼會出錯。但是如果 ?1? 輸入成?6?,有時校驗碼仍然正確。因為6=1+5,如果數(shù)字在奇數(shù)位,26=21+2\*5—數(shù)字和增加了10.同樣的錯誤也會發(fā)生在 ?7?代替 ?2?, ?8? 代替 ?3?, 等情況下。
第三步算法:再次計算數(shù)字和,但是讓奇數(shù)位...乘以3。比如代碼1234565 是有效的,因為1 + 2\*3 + 3 +? 4\*3 + 5 + 6\*3 +5 = 50.
這個方法略微變化成為了EAN13編碼的標準:數(shù)字位固定為13位,第13位為校驗位。奇數(shù)位數(shù)字相加3次,偶數(shù)位數(shù)字相加1次。
EAN-13條碼廣泛使用在貿(mào)易和商業(yè)領域,是人們最常見到的條碼編碼。Code-128編碼也使用同樣的校驗規(guī)則,具體條碼數(shù)據(jù)結構參見Wikipedia相關條目。
## 結論
正如我們所看到的,即使像條形碼這樣簡單的東西,也可以包含一些很酷的東西。順便給耐心地讀到這個地方的讀者一個小竅門——條形碼下的文本與條形碼數(shù)據(jù)完全相同。這是為操作員準備的,如果掃描器無法讀取,他們可以手動輸入代碼。因此很容易知道條形碼內容—— 只需閱讀條碼下面的文字。
------
原文:[How does a barcode work?](https://habr.com/en/post/439768/)