2018年由于某水利大省的水文数据网站改版之后,该省水文数据都改成13px高的小图片,比如下图这样的3张图片分别表示站名、上游水位、下游水位:
下载后用图片查看软件打开是这样的(91*13px,透明底,PNG格式):
最近,涉及Python学习的时候,发现python下各种图片识别技术已经很成熟。现有尝试过三种方法:
(1)各种云,比如阿里云、百度云、腾讯云。经过测试,仅仅腾讯云高精度版能够较为准确识别,百度云与阿里云不支持最短边小于15px的图片,利用python的Pillow模块resize放大后依旧无法准确识别。但是腾讯云的图片识别功能仅限企业注册,没有个人申请接口。个人免费使用需要注意额度限制,本次数据抓取,都是大量小图片,识别图片量较大,不划算。
(2)tesseract。tesseract是谷歌的一个开源识别框架。Tesseract OCR直接就是一个跨平台的命令行工具,可以直接运行,调试、测试都比较方便。相关第三方开发工具也很丰富,比如可以在python下使用的Pytesseract。但是经过测试,本次识别效果不太理想,对于中文识别尤其差,需要自己用训练库做ground truth,网上各种训练库的方法也很多。
(3)cnocr。cnocr 是 Python 3 下的中英文OCR工具包,自带了多个训练好的识别模型(最小模型仅 4.7M),安装后即可直接使用。cnocr v1.2 目前包含以下可直接使用的模型,训练好的模型都放在 cnocr-models 项目中,可免费下载使用:
模型名称 | 局部编码 | 序列编码模型 | 模型大小 | 迭代次数 | 测试集准确率 | 测试集中的图片预测速度(秒/张,环境:单GPU) |
---|---|---|---|---|---|---|
conv-lite-fc | conv-lite | fc | 18M | 25 | 98.61% | 0.004191 |
densenet-lite-gru | densenet-lite | gru | 9.5M | 39 | 99.04% | 0.003349 |
densenet-lite-fc | densenet-lite | fc | 4.7M | 40 | 97.61% | 0.003299 |
densenet-lite-s-gru | densenet-lite-s | gru | 9.5M | 35 | 98.52% | 0.002434 |
densenet-lite-s-fc | densenet-lite-s | fc | 4.7M | 40 | 97.20% | 0.002429 |
首先进行了初步直接测试,试用了多个模型后发现,densenet-lite-gru较好,但是依旧有大量错误。尝试过resize图片,放大10倍,透明底,效果依旧没有改观。用的是Pillow模块带的功能实现。
from PIL import Image
Imageurl = 'http://218.94.6.94:88/jsswxxSSI/static/map1/list/0/' + str(num) + '/' + id + '.png'
imageData = requests.get(Imageurl).content
imgBytes=BytesIO()
imgBytes.write(imageData)
im = Image.open(imgBytes)
im = im.resize((im.size[0] * 10, im.size[1] * 10), Image.ANTIALIAS)
后改为将透明底换成白底,用的是Pillow模块带的功能实现。识别效果依旧没有改善。
im = Image.open(imgBytes)
p = Image.new('RGBA', (im.size[0] , im.size[1] ), (255, 255, 255))
p.paste(im, (3, 3, im.size[0] , im.size[1] ), im)
p.save(r'%s%s.png' % (picPath, id))
最终发现,问题出在文字紧贴边界上。文字与图片边框之间没有留任何空隙影响了效果。需要在换白底的时候,把底放大,再把文字部分贴进去就好了。
最终识别结果仅仅个别符号识别错误,比如括号(由于紧贴文字无法正确识别,会把 (三 识别成 巨 等。稍加修正即可完善。初次用Python,代码质量比较粗糙。完整代码如下:
# -*- coding: utf-8 -*-
import re
import json
from io import BytesIO
import cnocr.consts
import requests
from PIL import Image
from cnocr import CnOcr
import mxnet as mx
dataList = []
ocr = CnOcr('densenet-lite-gru')
def getImageArr(img):
imgBytes = BytesIO()
imgBytes.write(img)
im = Image.open(imgBytes)
#图片处理部分,考虑到cnocr输入必须为文件地址或者图像数组,对输出做了完善。
p = Image.new('RGBA', (im.size[0] + 3, im.size[1] + 6), (255, 255, 255))
p.paste(im, (3, 3, im.size[0] + 3, im.size[1] + 3), im)
p = p.convert('RGB')
img_byte_arr = BytesIO()
p.save(img_byte_arr, format='PNG')
return mx.image.imdecode(img_byte_arr.getvalue(), 1)
def NameStrRepair(namestr):
if ('〉' in namestr):
namestr = namestr.replace('〉', ')')
if ('C' in namestr):
namestr = namestr.replace('C', '(')
if ('()' in namestr):
namestr = namestr.replace('()', '(新)')
if (')' in namestr) & ('(' not in namestr):
namestr = namestr[:(namestr.find(')') - 1)] + '(' + namestr[(namestr.find(')') - 1):]
if ('〉' in namestr):
namestr = namestr.replace('〉', ')')
if ('巨' in namestr):
namestr = namestr.replace('巨', '三')
if ('佬' in namestr):
namestr = namestr.replace('佬', '老')
if ('觅渡(桥)' in namestr):
namestr = namestr.replace('觅渡(桥)', '(觅渡桥)')
if ('钛' in namestr):
namestr = namestr.replace('钛', '太')
if ('傣' in namestr):
namestr = namestr.replace('傣', '泰')
if ('围(外' in namestr):
namestr = namestr.replace('围(外', '(围外')
if ('+─' in namestr):
namestr = namestr.replace('+─', '十一')
if ('锦(溪' in namestr):
namestr = namestr.replace('锦(溪', '(锦溪')
if ('锦(溪' in namestr):
namestr = namestr.replace('锦(溪', '(锦溪')
if ('江都抽水' in namestr):
namestr=namestr[:4]+'('+namestr[4]+')'+namestr[-1:]
return namestr
def getImageText(num, id):
Imageurl = 'http://218.94.6.94:88/jsswxxSSI/static/map1/list/0/' + str(num) + '/' + id + '.png'
imageData = requests.get(Imageurl).content
global ocr
txt = ''
try:
if num == 1:
ocr.set_cand_alphabet(None)
txt = ''.join(ocr.ocr_for_single_line(getImageArr(imageData)))
txt=NameStrRepair(txt)
else:
ocr.set_cand_alphabet(cnocr.consts.NUMBERS)
txt = ''.join(ocr.ocr_for_single_line(getImageArr(imageData)))
except:
pass
return txt
def getJSdata():
jsurl = 'http://218.94.6.94:88/jsswxxSSI/static/map/js/sqarea.min.js'
JSres = requests.get(jsurl)
re_json = re.compile(r'\[.*?.}\]')
JSjson = re_json.search(JSres.text).group(0)
JSdata = json.loads(JSjson, strict=False)
for ZDdata in JSdata:
data = {
}
data['id'] = ZDdata['id']
data['name'] = getImageText(1, ZDdata['id'])
data['upWL'] = getImageText(2, ZDdata['id'])
data['downWL'] = getImageText(3, ZDdata['id'])
dataList.append(data)
print(data)
# Press the green button in the gutter to run the script.
if __name__ == '__main__':
getJSdata()
运行会得到这样的结果:
{‘id’: ‘001d7e4c39a64d71b032017bb000ee44’, ‘name’: ‘丹阳’, ‘upWL’: ‘3.58’, ‘downWL’: ‘’}
{‘id’: ‘013a1c2aeb4146be8cc3f6beb0be5710’, ‘name’: ‘刘山南站水文’, ‘upWL’: ‘25.15’, ‘downWL’: ‘’}
{‘id’: ‘01a0223240f54009ae55bca5fbb6ff9f’, ‘name’: ‘常熟’, ‘upWL’: ‘3.22’, ‘downWL’: ‘’}
{‘id’: ‘01c24eff884e4a76a6360d589a07b720’, ‘name’: ‘十一圩港闸’, ‘upWL’: ‘3.36’, ‘downWL’: ‘’}
{‘id’: ‘023e1320526d443c8814051750a43f7f’, ‘name’: ‘阜宁腰闸’, ‘upWL’: ‘6.00’, ‘downWL’: ‘2.51’}
{‘id’: ‘02634643f4c344f4ae725d867d66f4c0’, ‘name’: ‘东台河闸’, ‘upWL’: ‘2.37’, ‘downWL’: ‘’}
{‘id’: ‘0273bb530b294a8e89c99fc53cde6702’, ‘name’: ‘沙集抽水站’, ‘upWL’: ‘20.50’, ‘downWL’: ‘14.16’}
{‘id’: ‘02c1a1d249cf4d81a8a45dd2c6686ad7’, ‘name’: ‘大套抽水二站’, ‘upWL’: ‘2.96’, ‘downWL’: ‘0.75’}
{‘id’: ‘02c3bcdbeaac4d55aefb870753069963’, ‘name’: ‘江都抽水(三)站’, ‘upWL’: ‘6.57’, ‘downWL’: ‘1.40’}
{‘id’: ‘02eecccefd71441d883bb3280a1c48df’, ‘name’: ‘韩庄(微)’, ‘upWL’: ‘32.25’, ‘downWL’: ‘’}
{‘id’: ‘037f3aa86dfe42059d8fe89570da7385’, ‘name’: ‘跋山’, ‘upWL’: ‘174.52’, ‘downWL’: ‘’}
{‘id’: ‘03d928a5f3c7483f835d444dba8c7a3c’, ‘name’: ‘无锡(大)’, ‘upWL’: ‘3.48’, ‘downWL’: ‘’}
{‘id’: ‘044f7507b64c4177a6d1f84126275fba’, ‘name’: ‘项庄’, ‘upWL’: ‘12.81’, ‘downWL’: ‘’}
{‘id’: ‘045b69369a74471e8634d9b563aca8b2’, ‘name’: ‘旧县’, ‘upWL’: ‘3.64’, ‘downWL’: ‘’}
{‘id’: ‘04934721a54b452d957b7850dfa0e30b’, ‘name’: ‘犊山闸’, ‘upWL’: ‘3.16’, ‘downWL’: ‘’}
{‘id’: ‘04b69a8f3e3a4fec85981f3511707208’, ‘name’: ‘大浦口’, ‘upWL’: ‘3.21’, ‘downWL’: ‘’}
{‘id’: ‘050861887f754a59802d84220935ef93’, ‘name’: ‘中滩橡胶坝’, ‘upWL’: ‘0.19’, ‘downWL’: ‘’}
{‘id’: ‘05e2d3bf2238427c9b33e5cf71abb7dc’, ‘name’: ‘仑山水库’, ‘upWL’: ‘48.52’, ‘downWL’: ‘’}
基本满足了我们的要求。
PS. cnocr的自己训练模型工作我也做了尝试,发现需要准备的模型文件太多,另外对电脑的计算性能要求比较高。我这台老笔记本T440实在无法胜任,最终放弃,但是有些坑可以帮大家填一下。自己训练的具体方法在cnocr的github页面都能看到,初次学习可能会有点看不懂。
(1)train.txt和test.txt文件格式
图片1.png label11 label12 label13…
图片2.png label21 label22 label23…
…
这里面的label,是指识别结果文字对应的文字库的序号,也就是识别库文件夹~/.cnocr/1.2.0/densenet-lite-gru/label_cn.txt 中对应文字或符号对应的序号。
(2)如何快速准备train.txt文件?
cnocr的GitHub页面并未给出快速准备该文件的方法,如果一个一个去手动查找,费时费力还容易出差。我是这样处理的:
先准备一个prefile文件,格式如下:
文件名 识别结果
(由于都是png后缀,我就省略了没写)示例如下:
01c24eff884e4a76a6360d589a07b720 十一圩港闸
02c3bcdbeaac4d55aefb870753069963 江都抽水(三)站
02eecccefd71441d883bb3280a1c48df 韩庄(微)
07cc368be8b74ba48e592fbbdd1d0c8b 江都抽水(二)站
17b1e5ba5d2e483d9243a6198c4ccef1 洞庭西山(三)
1b5b9ed42805414ea68ec746028231f5 大官庄闸(新)
2335a9568e8d4551a7c28c13a0f42ef9 望亭(立交)
25ed9afc53704b36b74c1f963382bb12 会宝岭(南)
2552698f2d8149ab88205cd09d6515c5 海安(串)
74f3dd2d57a643578525f270c4b2c998 常州(三)
7a019471354e4ccdac9e9f8daf34bc65 邵伯(大)
3e0420d8f29b42ae9917455ada34c94c 高邮(大)
4907e5ad311248c8adcfb7390f701a98 泰州(通)
4d91dd400f6e4297828a6b8bcb207261 高邮(高)
564602d366d24cbabea6d264714d782d 高邮(大)
58fbb1e8806347e1995552eb02090309 运西(电站)节制闸
5be39f0dc1bb4705952aaa17bde4ccc1 苏州(觅渡桥)
然后用了一段Python代码来生产train.txt文件。
# -*- coding: utf-8 -*-
#三个需要的文件目录
preFilePath=r'/media/nautilus/Data/Personal/Software/cnocr-1.2.2/cyctrain/prefile'
outputfile=r'/media/nautilus/Data/Personal/Software/cnocr-1.2.2/cyctrain/train.txt'
labelcnFP=r'/home/nautilus/.cnocr/1.2.0/densenet-lite-gru/label_cn.txt'
labeldict={
}
with open(labelcnFP) as flabelcn:
k=1
for line in flabelcn.readlines():
labeldata = line.strip()
labeldict[labeldata] = k
k=k+1
print (labeldict)
with open(preFilePath,'r') as f1:
with open(outputfile,'w') as f2:
for line in f1.readlines():
inputstr=line.strip().split(' ')
outputstr=inputstr[0]+'.png'
inputlabel=list(inputstr[1])
for labelchar in inputlabel:
outputstr=outputstr+' '+str(labeldict[labelchar])
f2.write(outputstr+'\n')
test.txt与train.txt 是一样的,不再重复。