怎样用Python识别条形码?[译]
# 怎样用Python识别条形码?[译]
现在每个人都在使用条形码,大家却几乎注意不到。当我们在商店买东西时,货品的识别使用条形码。仓库中的货物,邮政包裹等也同样使用条形码来识别。但实际上并没有多少人知道条形码是如何工作的。
条形码包含了什么内容,这个图像的编码内容是什么?
![](http://imgs.zhuotang.cc/img/zggiwtxnuelznl9cq6u9jzcio5m.jpeg)
让我们来弄清楚,并写出我们自己的解码器。
## 介绍
使用条形码已经有很长的历史。首次尝试实现自动化是在50年代完成的,当时一个读码系统被授予专利。在宾夕法尼亚铁路公司工作的大卫.柯林斯(David Collins)决定简化火车车厢分拣过程。他的方法是 —— 用不同的颜色条纹来编制车厢标识码,然后使用光电管读取它们。1962年这套编码成为美国铁路协会的标准(即KarTrak系统)。到了1968年,为了增加识别准确率,同时减小读码器的尺寸,采用激光替代了氙气灯。1973年开发出通用产品编码(UPC码),1974年第一个带条码的百货商品(箭牌口香糖)开始在美国销售。1984年条形码已经在全美商店使用,其他国家稍后也开始流行。
对于不同的应用,有不同的条码类型。比如字符串“12345678”可以被编码成下列这些条码(不是全部哟):
![](http://imgs.zhuotang.cc/img/20190320151853.png)
让我们开始分析。为了方便理解其原理,下面所有条形码均使用 Code-128 码。若想尝试其他编码,请使用 [在线条码生成器](https://barcode.tec-it.com/en/Code128) 自行处理。
初看条形码象一组随机的数字,实际上它的结构井井有条:
![](http://imgs.zhuotang.cc/img/20190320152125.png)
1 — 空白区,需要确定条码的起始位置。
2 — 开始位 。有三种Code-128类型可供选择(叫作A,B和C)。开始位相应分别是11010000100, 11010010000 或 11010011100 。不同类型的编码表是不同的(详见Code_128规范)
3 — 条码本身,包含用户数据。
4 — 校验位。
5 — 停止位。对于 Code-128是 1100011101011 。
6(1) — 空白区。
现在让我们来看看这些位是如何编码的。其实很简单——如果我们将最细的线宽设为 «1»,那么2倍的线宽就是«11»,3倍的线宽就是 «111»,以此类推。空白宽度按照同样原则,分别代表 «0», «00» 或 «000»。有兴趣的人可以比较上面图片验证规则是否有效。
现在我们可以开始编码了。
## 获得条码序列
一般来说,这是最复杂的部分,可以通过不同的方式实现。 我不确定我的方法是否是最优的,但对于我们的任务来说,这绝对是足够的。
首先,让我们加载图像,拉伸其宽度,从中间裁剪一条水平线,将其转换为黑白颜色并保存到数组中。
```
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»,所以数组中数据值需要倒置。另外我们还需要计算数组的平均值。
```
hor_data = 255 - hor_data
avg = np.average(hor_data)
plt.plot(hor_data)
plt.show()
```
让我们运行程序来验证条形码被正确加载:
![](http://imgs.zhuotang.cc/img/20190320152420.png)
现在我们需要确定一个数位的宽度。为此我们要提取数据,记录黑白线分界点的位置
```
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»,但是对于获取条码数位的像素宽度足够了。
现在让我们对数据进行解码。我们需要找到每个条码线,并找出其间距对应的位数。位数并不能精确匹配(条码会被拉伸或扭曲一点),所以我们需要将结果四舍五入为整数值。
```
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
```
也许有更好的方法来做到这一点,大家可以写到评论区。
如果一切都做得很完美,我们会得到类似的序列:
```
11010010000110001010001000110100010001101110100011011101000111011011
01100110011000101000101000110001000101100011000101110110011011001111
00010101100011101011
```
## 解码
一般来说,解码很容易。Code-128码是11位条码,具有不同的编码类型(根据编码类型—A,B或C,可以表示字母或[00]-[99]的数字对集合。
在我们的例子中,起始位是 11010010000,对应编码类型B。我懒得手动输入所有代码,所以直接从维基百科页面上复制粘贴它。解析每行的内容也是使用Python(提示—开发产品可别这么干)
```
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位数据块:
```
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)
```
我没有在此显示本文开头条码图片的解码结果,把它作为读者的作业吧(使用下载的智能手机APP识别将被视为作弊:)
CRC校验也没有在此代码中实现,如有需要请自行解决。
当然,本算法并不完美,它只花了一个半小时完成。对于专业性任务可以使用现成的类库,比如 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%条码能够被正确验证。
第一步算法:让我们得到数字和,且余数为0.第一个符号包含数据,最后一个数字是这样选择的,数字和除以10。解码后,如果数字和不能被10整除,则解码错误,需要重新解码。比如,条码1234有效—1+2+3+4 = 10。条码1216也有效,但1218无效。
这避免了解码问题。但是条码可以通过硬件键盘手工输入。这个方法的另一个缺陷被发现——如果订单的两位数字被交换了,校验位仍然正确,这太糟了。不如,代码1234被输入为2134,校验位仍然是一样的。如果人们输入数字很快,错误的数字顺序是常见的情况。
第二步算法:改进校验位算法——计算奇数位两次。这样,如果订单号改变了,数字和就不对了。比如代码2364是有效的(2 + 3\*2+ 6 + 4\*2 = 22),但代码3264是无效的(3 + 2\*2 + 6 + 4\*2 = 21)。很好,但是另一种情况又出现了。有些键盘是两行10键,第一行是12345,第二行是67890.如果 «1» 输入成«2»,检验码会出错。但是如果 «1» 输入成«6»,有时校验码仍然正确。因为6=1+5,如果数字在奇数位,26=21+2\*5—数字和增加了10.同样的错误也会发生在 «7»代替 «2», «8» 代替 «3», 等情况下。
第三步算法:再次计算数字和,但是让奇数位...乘以3。比如代码1234565 是有效的,因为1 + 2\*3 + 3 + 4\*3 + 5 + 6\*3 +5 = 50.
这个方法略微变化成为了EAN13编码的标准:数字位固定为13位,第13位为校验位。奇数位数字相加3次,偶数位数字相加1次。
EAN-13条码广泛使用在贸易和商业领域,是人们最常见到的条码编码。Code-128编码也使用同样的校验规则,具体条码数据结构参见Wikipedia相关条目。
## 结论
正如我们所看到的,即使像条形码这样简单的东西,也可以包含一些很酷的东西。顺便给耐心地读到这个地方的读者一个小窍门——条形码下的文本与条形码数据完全相同。这是为操作员准备的,如果扫描器无法读取,他们可以手动输入代码。因此很容易知道条形码内容—— 只需阅读条码下面的文字。
------
原文:[How does a barcode work?](https://habr.com/en/post/439768/)