使用百度 OCR 录入课堂考勤信息
目录
之前在做助教的时候,我写了一个小工具来统计参加课程的学生人数。因为班上有将近一百个人,每次上课都会以小组为单位做一些题目,并且需要每个人写上自己的姓名和学号。如果要在一百个人中去检索有哪些人没有出勤,不管是在 Excel 里面找,还是再去做对比,都需要花比较多的功夫。所以我就想到了用程序来解决这个问题。
基本思路是调用百度的 OCR 接口去做图像文字识别。它会识别出来学生的名字和学号等信息,并以列表返回。然后我会把所有人的姓名学号跟识别的结果做一个编辑距离的匹配,计算出每个人的权重最后保留较高的几个。其中学号占的比重比较高,因为数字的识别精度相对较高。而姓名占的比重比较低,因为可能有几个人会有一些相近的名字,而且名字本身的长度也偏低,容易误判。
写完这些之后,我用 tkinter 做了一个 GUI 界面封装。界面上有几个按钮:第一个按钮是点击按钮进行拍照;第二个按钮是根据拍照结果自动生成学生名单;第三个按钮是可以点击图片上的名字进行保存;最后一个按钮是导出按钮,点击之后会生成一个文本文档,然后可以拖到 Excel 里面去做匹配,查看有谁缺勤。
这个小工具虽然实现了基本的功能,但是实际使用起来有以下几个问题:
- OCR 识别精度不高。对于字体比较乱、行间距比较密集的情况,识别结果会有一些偏差。所以需要引入编辑距离的概念,进行匹配和校正。
- Web API 的使用形式比较麻烦。需要去申请一个账号,并且根据官方的说明文档申请对应的token。
- 扫描操作比较繁琐。需要频繁地点击几个按钮,才能完成一次扫描。而且扫描的时间可能并没有比人工分工去做这些事情快多少。
关于具体的效率问题,我个人感觉是难以评判的点。因为如果在 Excel 里面去找名单做对比的话,随着时间的推移,需要找的位置会越来越少,到最后那几个缺勤的位置就很容易发现了。但是用这个小工具的话,不管哪个位置都需要用几乎相等的时间去做记录。因为没有具体的比较,所以很难说哪个更好。
from fuzzywuzzy import fuzz
import tkinter as tk
import cv2
from PIL import Image,ImageTk
import random
# ==================== 参数 ====================
ACCESS_TOKEN = "self-token"
NAME_RATIO = 0.5 # 控制姓名匹配度的权重
# ==================== 函数定义 ====================
camera = cv2.VideoCapture(0)
# 保存最终结果
final_stu = []
# 加载学生数据
people_dict = {}
with open("people.txt", "r", encoding="utf8") as fh:
lines = fh.readlines()
for line in lines:
line = line.strip()
line = line.split(',')
people_dict[line[1]] = line[0]
def paddle_ocr():
"""调用百度OCR接口进行识别"""
request_url = "https://aip.baidubce.com/rest/2.0/ocr/v1/accurate_basic"
# 二进制方式打开图片文件
f = open('test.jpg', 'rb')
img = base64.b64encode(f.read())
params = {"image":img}
access_token = ACCESS_TOKEN
request_url = request_url + "?access_token=" + access_token
headers = {'content-type': 'application/x-www-form-urlencoded'}
response = requests.post(request_url, data=params, headers=headers)
if response:
print (response.json())
tmp = response.json()
tmp = tmp['words_result']
res = []
for n in tmp:
res.append(n['words'])
return res
def get_score(ocr_res):
"""根据识别结果对所有学生进行匹配并保存数值"""
stu_id = list(people_dict.keys())
score = []
for o in ocr_res:
for i in range(len(stu_id)):
# 尝试用查询结果和每个人的学号和姓名进行匹配并累加到分数数组中 跳过已经记录的同学
name = people_dict[stu_id[i]]
if name in final_stu:
continue
cur = 0 # 当前识别结果与学生的匹配度
cur = cur + fuzz.ratio(o, stu_id[i])
cur = cur + NAME_RATIO * fuzz.ratio(o, people_dict[stu_id[i]])
score.append((people_dict[stu_id[i]], cur))
# 目前算法是每个识别结果与姓名学号均进行匹配后保存,结果中会有较多重复数据,代表同一学生对不同识别结果的匹配度
# 是否需要改成得分数组累加,使得分数组长度等于学生长度
print(score)
# 根据学号排序保存前6个最大值
res = []
score.sort(key=lambda x : x[1], reverse=True)
for i in range(6):
if i >= len(score):
break
else:
res.append(score[i][0])
return res
def add_stu(stu):
"""向结果中添加学生"""
if stu not in final_stu:
final_stu.append(stu)
def export_stu():
"""导出结果"""
with open("res.txt", 'w+', encoding='utf8') as fh:
for n in final_stu:
fh.write(n + '\n')
def add_button():
"""根据识别结果添加按钮"""
VIDEO_STOP = True
success, img = camera.read()
cv2.imwrite('test.jpg',img)
ocr_res = paddle_ocr()
res = get_score(ocr_res)
for index, name in enumerate(res):
tk.Button(win, width=10, height=1, text=name, command=lambda v=name : add_stu(v)).grid(row=1, column=index + 1)
# ==================== GUI ====================
def video_loop():
success, img = camera.read() # 从摄像头读取照片
if success:
cv2.waitKey(20)
cv2image = cv2.cvtColor(img, cv2.COLOR_BGR2RGBA)#转换颜色从BGR到RGBA
current_image = Image.fromarray(cv2image)#将图像转换成Image对象
imgtk = ImageTk.PhotoImage(image=current_image)
panel.imgtk = imgtk
panel.config(image=imgtk)
win.after(1, video_loop)
win = tk.Tk()
panel = tk.Label(win) # initialize image panel
panel.grid(row=0, column=0)
tk.Button(win, width=10, height=1, text="add", command=add_button).grid(row=0, column=1)
tk.Button(win, width=10, height=1, text="export", command=export_stu).grid(row=0, column=3)
video_loop()
win.mainloop()