稍微整理了一下这个系列的一二三四章,可能看着更舒服吧……这个系列的解决方案不止一种,调参的方法也是各种各样,反正能够满足需求就极好了
1.预处理
这次的机读卡识别项目来源暑期培训,主要包括内容一张手机拍摄的机读卡位置定位,识别其中选择题模块及少量数字识别,给出样例图片:
预处理目的:
对于这个识别问题而言,把图像变成二值图应该是最简单粗暴的方法了。为了找准边界,才能良好切割。而对于边缘检测的函数也只能传入灰度图……
1.1.环境配置:
环境是python3.5的,大体部分需要配置的是numpy+mlk版本,scipy,来支持opencv,另外辅助以imutils,这个包里面含有4点变换函数以及matplotlib来辅助绘图
import cv2
import matplotlib.pyplot as plt
import imutils
from imutils.perspective import four_point_transform
1.2.图片预处理
为了方便找出图片的4个顶点,所以需要一次自适应二值化,为了使图片效果更好,所以在二值化之前还加了一层高斯滤波
#读入图片
image = cv2.imread("test10.jpg")
#转换为灰度图像
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
#高斯滤波
blurred = cv2.GaussianBlur(gray, (3, 3), 0)
#自适应二值化方法
blurred=cv2.adaptiveThreshold(blurred,255,cv2.ADAPTIVE_THRESH_MEAN_C,cv2.THRESH_BINARY,51,2)
'''
adaptiveThreshold函数:第一个参数src指原图像,原图像应该是灰度图。
第二个参数x指当像素值高于(有时是小于)阈值时应该被赋予的新的像素值
第三个参数adaptive_method 指: CV_ADAPTIVE_THRESH_MEAN_C 或 CV_ADAPTIVE_THRESH_GAUSSIAN_C
第四个参数threshold_type 指取阈值类型:必须是下者之一
• CV_THRESH_BINARY,
• CV_THRESH_BINARY_INV
第五个参数 block_size 指用来计算阈值的象素邻域大小: 3, 5, 7, ...
第六个参数param1 指与方法有关的参数。对方法CV_ADAPTIVE_THRESH_MEAN_C 和 CV_ADAPTIVE_THRESH_GAUSSIAN_C, 它是一个从均值或加权均值提取的常数, 尽管它可以是负数。
'''
#这一步可有可无,主要是增加一圈白框,以免刚好卷子边框压线后期边缘检测无果。好的样本图就不用考虑这种问题
blurred=cv2.copyMakeBorder(blurred,5,5,5,5,cv2.BORDER_CONSTANT,value=(255,255,255))
2.图像切割
图像切割的目的是将图像定个便于识别的样子。比如这里四角变换结束以后会吧图像变为2400*2800的大小,无论是什么样的案例图片,都是这个格式,这样最后在局部分割,如选择题答案的识别和数字区域的确定这套程序才能有较好的通用性
2.1.边缘检测
预处理得到二值图像就很容易做边缘检测了,找出4个点,方便之后的4点变换
#canny边缘检测
edged = cv2.Canny(blurred, 10, 100)
# 从边缘图中寻找轮廓,然后初始化答题卡对应的轮廓
'''
findContours
image -- 要查找轮廓的原图像
mode -- 轮廓的检索模式,它有四种模式:
cv2.RETR_EXTERNAL 表示只检测外轮廓
cv2.RETR_LIST 检测的轮廓不建立等级关系
cv2.RETR_CCOMP 建立两个等级的轮廓,上面的一层为外边界,里面的一层为内孔的边界信息。如果内孔内还有一个连通物体,
这个物体的边界也在顶层。
cv2.RETR_TREE 建立一个等级树结构的轮廓。
method -- 轮廓的近似办法:
cv2.CHAIN_APPROX_NONE 存储所有的轮廓点,相邻的两个点的像素位置差不超过1,即max (abs (x1 - x2), abs(y2 - y1) == 1
cv2.CHAIN_APPROX_SIMPLE压缩水平方向,垂直方向,对角线方向的元素,只保留该方向的终点坐标,例如一个矩形轮廓只需
4个点来保存轮廓信息
cv2.CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS使用teh-Chinl chain 近似算法
'''
cnts = cv2.findContours(edged, cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if imutils.is_cv2() else cnts[1]
docCnt = None
# 确保至少有一个轮廓被找到
if len(cnts) > 0:
# 将轮廓按大小降序排序
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)
# 对排序后的轮廓循环处理
for c in cnts:
# 获取近似的轮廓
peri = cv2.arcLength(c, True)
approx = cv2.approxPolyDP(c, 0.02 * peri, True)
# 如果近似轮廓有四个顶点,那么就认为找到了答题卡
if len(approx) == 4:
docCnt = approx
break
顶点坐标的存放形式为3维数组,所以若想演示最大的4个顶点应做如下操作:
newimage=image.copy()
for i in docCnt:
#circle函数为在图像上作图,新建了一个图像用来演示四角选取
cv2.circle(newimage, (i[0][0],i[0][1]), 50, (255, 0, 0), -1)
2.2.四点变换
四点变换直接调用大佬写好放在imutils中的函数就好了。这里存了两个,一个原图一个灰度图,原图用来配合展示,灰度图用来支配
paper = four_point_transform(image, docCnt.reshape(4, 2))
warped = four_point_transform(gray, docCnt.reshape(4, 2))
3.对选择题识别
3.1.对选择题图像部分预处理
经过四点变换后的图像需要经过重新转换标准长宽,以便对选择题部分标定题号及答案。这里的图像定义为2400*2800 。选择题部分最大的特点是需要将黑块突出,以及过滤掉没填涂的选项,以便确认。预处理方法选择均值滤波及二进制二值化的方法。
# 对灰度图应用二值化算法
thresh=cv2.adaptiveThreshold(warped,255,cv2.ADAPTIVE_THRESH_MEAN_C,cv2.THRESH_BINARY,53,2)
#重塑可能用到的图像
thresh = cv2.resize(thresh, (width1, height1), cv2.INTER_LANCZOS4)
paper = cv2.resize(paper, (width1, height1), cv2.INTER_LANCZOS4)
warped = cv2.resize(warped, (width1, height1), cv2.INTER_LANCZOS4)
#均值滤波
ChQImg = cv2.blur(thresh, (23, 23))
#二进制二值化
ChQImg = cv2.threshold(ChQImg, 100, 225, cv2.THRESH_BINARY)[1]
'''
threshold参数说明
第一个参数 src 指原图像,原图像应该是灰度图。
第二个参数 x 指用来对像素值进行分类的阈值。
第三个参数 y 指当像素值高于(有时是小于)阈值时应该被赋予的新的像素值
第四个参数 Methods 指,不同的不同的阈值方法,这些方法包括:
•cv2.THRESH_BINARY
•cv2.THRESH_BINARY_INV
•cv2.THRESH_TRUNC
•cv2.THRESH_TOZERO
•cv2.THRESH_TOZERO_INV
'''
3.2.寻找结果中黑块坐标
这里寻找坐标的目的是为了确定黑块所代表的题号及选项,用轮廓中心来进行描述
# 在二值图像中查找轮廓
cnts = cv2.findContours(ChQImg, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if imutils.is_cv2() else cnts[1]
for c in cnts:
# 计算轮廓的边界框,然后利用边界框数据计算宽高比
(x, y, w, h) = cv2.boundingRect(c)
if (w > 60 & h > 20)and y>900 and y<2000:
M = cv2.moments(c)
cX = int(M["m10"] / M["m00"])
cY = int(M["m01"] / M["m00"])
#绘制中心及其轮廓
cv2.drawContours(paper, c, -1, (0, 0, 255), 5, lineType=0)
cv2.circle(paper, (cX, cY), 7, (255, 255, 255), -1)
#保存题目坐标信息
Answer.append((cX, cY))
3.3.计算选择题题号及答案
比较绕,主要还是根据取余和倍数关系
def judgey0(y):
if (y / 5 < 1):
return y + 1
elif y / 5 < 2 and y/5>=1:
return y % 5 + 20 + 1
else:
return y % 5 + 40 + 1
def judgex0(x):
if(x%5==1):
return 'A'
elif(x%5==2):
return 'B'
elif(x%5==3):
return 'C'
elif(x%5==4):
return 'D'
def judge0(x,y):
if x/5<1 :
#print(judgey0(y))
return (judgey0(y),judgex0(x))
elif x/5<2 and x/5>=1:
#print(judgey0(y)+5)
return (judgey0(y)+5,judgex0(x))
elif x/5<3 and x/5>=2:
# print(judgey0(y)+10)
return (judgey0(y)+10,judgex0(x))
else:
#print(judgey0(y)+15)
return (judgey0(y)+15,judgex0(x))
输出运算结果:
IDAnswer=[]
for i in Answer:
for j in range(0,len(xt1)-1):
if i[0]>xt1[j] and i[0]
for k in range(0,len(yt1)-1):
if i[1]>yt1[k] and i[1]
judge0(j,k)
IDAnswer.append(judge0(j,k))
#对答案部分重新排序,以最好的方式输出
IDAnswer.sort()
print(IDAnswer)
print(len(IDAnswer))
至此完成选择题部分
4.数字识别,调用百度api
数字识别经过测试总的感觉还是可以。需要注意的地方是要对数字板块需要切割出来这样给机器会好认点,但也不能单个字拿出来。最好能有一串,同时也需要注意图片尺寸
4.1.对数字图像部分进行处理
预处理部分同样需要,步骤与选择题模块相似,但目的不同,文字部分主要将数字变粗,便于识别。其实也就是和选择题模块相比变了几个参数
NumImg=cv2.blur(thresh,(15,15))
NumImg=cv2.threshold(NumImg, 170, 255, cv2.THRESH_BINARY)[1]
4.2.调用百度ocr api
试过多种检测方式,还是用别人家现成的好http://apistore.baidu.com/
百度api使用方法:
首先需要注册一个百度云账号,这样在个人中心里就会看到apikey。这个就是和百度进行交流的钥匙。然后找到百度ocr的入口找到接口地址。虽然这里给出了但还是可以看下文档,里面有些细节,比如图片想免费就要300k以内。上面给的python示例代码是py2的,这里给出py3的方式(http://apis.baidu.com/idl_baidu/baiduocrpay/idlocrpaid)
import sys, urllib, json
import urllib.request
import urllib.parse
import base64
url = 'http://apis.baidu.com/idl_baidu/ocridcard/ocridcard'
data = {}
data['fromdevice'] = "pc"
data['clientip'] = "10.10.10.0"
data['detecttype'] = "LocateRecognize"
data['languagetype'] = "ENG"#英文模式
data['imagetype'] = "1"
#图片在本地
file_object = open('T.png','rb')
try:
img = file_object.read( )
finally:
file_object.close( )
data['image'] =base64.b64encode(img)
decoded_data = urllib.parse.urlencode(data)
decoded_data = decoded_data.encode('utf-8')
req = urllib.request.Request(url,decoded_data)
req.add_header("Content-Type", "application/x-www-form-urlencoded")
req.add_header("apikey", "这里填入个人中心的apikey")
resp = urllib.request.urlopen(req)
content = resp.read()
if(content):
content = json.loads(content.decode())
print(content)
4.3.切割图片
根据具体情况需要切割图片才能让百度api识别,具体限制因素还是图片大小,切割方式,这里只给出示例
#切割具体位置[起始y:终止y,起始x:终止y]
tempimg1=img[240:461,213:939]
#图片切割,width,height分别填入目标宽高
tempimg1 = cv2.resize(tempimg1, (width, height), cv2.INTER_LANCZOS4)
#图片保存,png,jpg格式均可
cv2.imwrite("T.png", tempimg1)
之后调用,若识别为英文需要转化,比如可能将0识别为D,这时转换即可,如:
def temp(char):
if(char=='D'):
return '0'
效果如图展示
当然若是能想办法去掉答题卡外围边框效果应该会更好……