赞
踩
利用python+opencv对答题卡进行检测,圈出正确的答案,并打印出得分。
原始图像:
最终结果:
读入图像并转化为灰度图
# 读入图像
img_org = cv2.imread(img_path)
cv_show('img_org', img_org)
img = cv2.cvtColor(img_org, cv2.COLOR_BGR2GRAY)
主要对图像进行去噪和透视变换
'''图像预处理'''
# 高斯滤波除去噪点
img = cv2.GaussianBlur(img, (3,3), 0, 0)
cv_show('img', img)
然后为透视变换做些准备
首先是边缘和轮廓检测
# 边缘检测
img = cv2.Canny(img, 20, 200)
cv_show('img_canny', img)
# 轮廓检测
contours, __ = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# img_copy = img.copy()
# img_copy = cv2.drawContours(img_copy, contours, -1, (0,255,0), 2)
# cv_show('img_con', img_copy)
'''我的opencv版本是4.1.0轮廓检测的返回值是二元组,opencv3则返回三元组'''
我们需要对找到的轮廓进行筛选,找到答题纸部分的轮廓
一般最大的面积的轮廓就是需要的答题纸部分
对答题纸部分的轮廓进行近似,将不太规整的轮廓转化为四边形
# 遍历所有轮廓找出面积最大的轮廓 if len(contours) > 0: # 根据cv2.contourArea函数进行降序排序 cnt = sorted(contours, key= cv2.contourArea, reverse= True) # 遍历轮廓 for i in cnt: # 计算周长 long = cv2.arcLength(i, closed= True) # 近似轮廓为折线 approx = cv2.approxPolyDP(i, 0.02 * long, closed= True) # 检测返回的折线坐标 if len(approx) == 4: docCnt = approx print(docCnt.shape) # >>>(4, 1, 2) break
在进行透视变换前,还需要计算些参数
这里稍微介绍下opencv4中做透视变换的两个函数
1.cv2.getPerspectiveTransform(src,M)
src表示原图像的四边顶点的坐标,M表示为要求变换的四边顶点的坐标,最后得到一个3x3的变换矩阵
2.cv2.warpPerspective(src,M,dsize(height,width)
src为输入图像,M为cv2.getperpectiveTransform()函数的到的变换矩阵),dsize为输出图像的大小
学习笔记4(opencv and python 透视变换(鸟瞰))
按照这两个函数需要的参数,我们需要通过原图的四个顶点坐标、待变换图像四个顶点坐标放入cv2.getPerspectiveTransform(src,M)中得到3x3的变换矩阵。
然后我们需要得到输出图像的尺寸也就是变换后图像的长和宽,再加上上一个函数得到的变换矩阵就可以用cv2.warpPerspective(src,M,dsize(height,width)来得到透视变换后的图像。
'''按照上面的思路,写两个函数来实现透视变换''' # 获取要变换图像的四点坐标 def get_point(pot): ret = np.zeros((4,2), dtype= 'float32') # 按列相加就是(x+y)横坐标与列坐标相加 a = np.sum(pot, axis= 1) # 小的是左上坐标 ret[0] = pot[np.argmin(a)] # 大的是右下坐标 ret[2] = pot[np.argmax(a )] # 按列相减就是|x-y|横坐标和纵坐标相减 a = np.diff(pot, axis= 1) # 小的是右上坐标 ret[1] = pot[np.argmin(a)] # 大的是左下坐标 ret[3] = pot[np.argmax(a)] return ret # 透视变换 def Perspective_transformation(img, pot): pot = get_point(pot) # 获取坐标 p1, p2, p3, p4 = pot # 获取待处理图片的各个宽度和长度 width2 = int(np.sqrt((p4[0] - p1[0]) ** 2 + (p4[1] - p1[1]) ** 2)) width1 = int(np.sqrt((p3[0] - p2[0]) ** 2 + (p3[1] - p2[1]) ** 2)) height1 = int(np.sqrt((p2[0] - p1[0]) ** 2 + (p2[1] + p1[1]) ** 2)) height2 = int(np.sqrt((p3[0] - p4[0]) ** 2 + (p3[1] - p4[1]) ** 2)) # 得到最大宽度和长度 width_max = int(max(width1, width2)) height_max = int(max(height1, height2)) # 定义处理后图像的坐标 pot_aft = np.array( ([0,0], [width_max - 1, 0], [width_max - 1, height_max - 1], [0, height_max - 1]), dtype= 'float32' ) # 获取变换矩阵 m = cv2.getPerspectiveTransform(pot, pot_aft) # 透视变换 warped = cv2.warpPerspective(img, m, (width_max, height_max)) return warped # 进行透视变换修正图像 # 二值图 img_per = Perspective_transformation(img, docCnt.reshape((4,2))) cv_show('img_per', img_per) # 彩色图 img_color_per = Perspective_transformation(img_org, docCnt.reshape((4,2))) cv_show('img_color_per', img_color_per)
得到透视变换后的二值图:
得到透视变换后的彩色图:
在透视变换后的图像上,各个选项的形状大致是差不多的,所以只需要规定一些条件就可以把选项图像筛选出来,在此之前则需要得到选项的尺寸以及坐标,采取的方法为轮廓检测。
contours, __ = cv2.findContours(img_bin, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
answer_pos = []
for i in contours:
# 得到各个轮廓的外接矩形的特征
x, y, w, h = cv2.boundingRect(i)
a = w / h
# 筛选条件为长和宽的大小以及比例
if w >= 20 and h >= 20 and a >= 0.9 and a <= 1.1:
answer_pos.append(i)
# print('answer_pos', answer_pos)
得到选项的图像轮廓之后,需要做一些排序,因为所获取的坐标顺序可能不符合实际选项的顺序。
观察图中选项的位置,同一道题目的不同选项,其纵坐标不同横坐标相同,不同题目同一列的选项纵坐标相同而横坐标不同。所以排序坐标也就可以排序选项的轮廓
# 轮廓排序 def sort_contours(cnt, method= "left-to-right"): reverse = False i = 0 if method == "right-to-left" or method == "bottom-to-top": reverse = True if method == "top-to-bottom" or method == "bottom-to-top": i = 1 # 获取轮廓信息 bound = [cv2.boundingRect(c) for c in cnt] # print(cnt) print(bound) # 按照y纵坐标进行排序 (cnt, bound) = zip(*sorted(zip(cnt, bound), key= lambda b: b[1][i], reverse= reverse)) print(bound) return cnt, bound answer_pos, __ = sort_contours(answer_pos, method= "top-to-bottom") # print(answer_pos)
在原图,被选出来的选项被涂黑,而在二值图下,被选出来的选项反而比较白(也可能白底黑选项,取决于阈值检测),所以可以通过判断白像素点的数目来得到被选出的那个选项,然后与正确选项对比就可以得到最终得分。
correct = 0 # 利用枚举获取每一行的选项 # np.arange返回的是一个序列 print('len(answer_pos)', len(answer_pos)) for i, j in enumerate(np.arange(0, len(answer_pos), 5)): print(i, j) # 获取每一行的轮廓 ants = sort_contours(answer_pos[j: j+5])[0] bubbled = None for q, j in enumerate(ants): print(q) # 制作掩膜 mask = np.zeros(img_bin.shape, dtype= 'uint8') mask = cv2.drawContours(mask, [j], -1, 255, -1) # cv_show('mask', mask) 可以去掉注释观察下掩膜。就是选项所在的位置是白色其他为黑色 # 保留答案部分 img_mask = cv2.bitwise_and(img_bin, img_bin, mask= mask) # cv_show('img_mask', img_mask) 可以去掉注释观察下。就是只保留了掩膜选项部分的图像其他部分为黑 # 返回灰度值不为0的像素数目 total = cv2.countNonZero(img_mask) if bubbled is None or total > bubbled[0]: # 保存这个选项的白像素数目和在这道题目的选项索引 bubbled = (total, q) # 得到正确答案代表的索引 answer = ANSWER_KEY[i] if bubbled[1] == answer: correct += 1 color = (0, 255, 0) # 标出正确的答案 img_color_per = cv2.drawContours(img_color_per, ants, q, color, thickness=2)
得到的正确选项的图像
all_answer = (correct / 5) * 100
print(all_answer)
cv_show('img_color_per', img_color_per)
# 打印最后的得分
img_color_per = cv2.putText(img_color_per, 'your grade:'+str(all_answer)+'%',(10,10), cv2.FONT_HERSHEY_COMPLEX, 0.6, (255,0,0), 1, bottomLeftOrigin= False)
cv_show('img_color_per_grade', img_color_per)
最终成果:
import numpy as np import cv2 img_path = './test_01.png' # 正确答案 ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1} def cv_show(name, img): cv2.imshow(name, img) cv2.waitKey(0) cv2.destroyAllWindows() # 获取要变换图像的四点坐标 def get_point(pot): ret = np.zeros((4,2), dtype= 'float32') # 按列相加就是(x+y)横坐标与列坐标相加 a = np.sum(pot, axis= 1) # 小的是左上坐标 ret[0] = pot[np.argmin(a)] # 大的是右下坐标 ret[2] = pot[np.argmax(a )] # 按列相减就是|x-y|横坐标和纵坐标相减 a = np.diff(pot, axis= 1) # 小的是右上坐标 ret[1] = pot[np.argmin(a)] # 大的是左下坐标 ret[3] = pot[np.argmax(a)] return ret # 透视变换 def Perspective_transformation(img, pot): pot = get_point(pot) # 获取坐标 p1, p2, p3, p4 = pot # 获取待处理图片的各个宽度和长度 width2 = int(np.sqrt((p4[0] - p1[0]) ** 2 + (p4[1] - p1[1]) ** 2)) width1 = int(np.sqrt((p3[0] - p2[0]) ** 2 + (p3[1] - p2[1]) ** 2)) height1 = int(np.sqrt((p2[0] - p1[0]) ** 2 + (p2[1] + p1[1]) ** 2)) height2 = int(np.sqrt((p3[0] - p4[0]) ** 2 + (p3[1] - p4[1]) ** 2)) # 得到最大宽度和长度 width_max = int(max(width1, width2)) height_max = int(max(height1, height2)) # 定义处理后图像的坐标 pot_aft = np.array( ([0,0], [width_max - 1, 0], [width_max - 1, height_max - 1], [0, height_max - 1]), dtype= 'float32' ) # 获取变换矩阵 m = cv2.getPerspectiveTransform(pot, pot_aft) # 透视变换 warped = cv2.warpPerspective(img, m, (width_max, height_max)) return warped # 轮廓排序 def sort_contours(cnt, method= "left-to-right"): reverse = False i = 0 if method == "right-to-left" or method == "bottom-to-top": reverse = True if method == "top-to-bottom" or method == "bottom-to-top": i = 1 # 获取轮廓信息 bound = [cv2.boundingRect(c) for c in cnt] # print(cnt) print(bound) # 按照y纵坐标进行排序 (cnt, bound) = zip(*sorted(zip(cnt, bound), key= lambda b: b[1][i], reverse= reverse)) print(bound) return cnt, bound # 读入图像 img_org = cv2.imread(img_path) cv_show('img_org', img_org) img = cv2.cvtColor(img_org, cv2.COLOR_BGR2GRAY) '''图像预处理''' # 高斯滤波除去噪点 img = cv2.GaussianBlur(img, (3,3), 0, 0) cv_show('img', img) # 边缘检测 img = cv2.Canny(img, 20, 200) cv_show('img_canny', img) # 轮廓检测 contours, __ = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # img_copy = img.copy() # img_copy = cv2.drawContours(img_copy, contours, -1, (0,255,0), 2) # cv_show('img_con', img_copy) # 遍历所有轮廓找出面积最大的轮廓 if len(contours) > 0: # 根据cv2.contourArea函数进行降序排序 cnt = sorted(contours, key= cv2.contourArea, reverse= True) # 遍历轮廓 for i in cnt: # 计算周长 long = cv2.arcLength(i, closed= True) # 近似轮廓为折线 approx = cv2.approxPolyDP(i, 0.02 * long, closed= True) if len(approx) == 4: docCnt = approx print(docCnt.shape) break # 进行透视变换修正图像 # 二值图 img_per = Perspective_transformation(img, docCnt.reshape((4,2))) cv_show('img_per', img_per) # 彩色图 img_color_per = Perspective_transformation(img_org, docCnt.reshape((4,2))) cv_show('img_color_per', img_color_per) # 转化为二值图 __, img_bin = cv2.threshold(img_per, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) cv_show('img_bin', img_bin) contours, __ = cv2.findContours(img_bin, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) answer_pos = [] for i in contours: x, y, w, h = cv2.boundingRect(i) a = w / h if w >= 20 and h >= 20 and a >= 0.9 and a <= 1.1: answer_pos.append(i) # print('answer_pos', answer_pos) answer_pos, __ = sort_contours(answer_pos, method= "top-to-bottom") # print(answer_pos) correct = 0 # 利用枚举获取每一行的选项 # np.arange返回的是一个序列 print('len(answer_pos)', len(answer_pos)) for i, j in enumerate(np.arange(0, len(answer_pos), 5)): print(i, j) # 获取每一行的轮廓 ants = sort_contours(answer_pos[j: j+5])[0] bubbled = None for q, j in enumerate(ants): print(q) # 制作掩膜 mask = np.zeros(img_bin.shape, dtype= 'uint8') mask = cv2.drawContours(mask, [j], -1, 255, -1) # cv_show('mask', mask) # 保留答案部分 img_mask = cv2.bitwise_and(img_bin, img_bin, mask= mask) # cv_show('img_mask', img_mask) # 返回灰度值不为0的像素数目 total = cv2.countNonZero(img_mask) if bubbled is None or total > bubbled[0]: # 保存这个选项的白像素数目和在这道题目的选项索引 bubbled = (total, q) # 得到正确答案代表的索引 answer = ANSWER_KEY[i] if bubbled[1] == answer: correct += 1 color = (0, 255, 0) # 标出正确的答案 img_color_per = cv2.drawContours(img_color_per, ants, q, color, thickness=2) all_answer = (correct / 5) * 100 print(all_answer) cv_show('img_color_per', img_color_per) # 打印最后的得分 img_color_per = cv2.putText(img_color_per, 'your grade:'+str(all_answer)+'%',(10,10), cv2.FONT_HERSHEY_COMPLEX, 0.6, (255,0,0), 1, bottomLeftOrigin= False) cv_show('img_color_per_grade', img_color_per)
若有什么错误,还请评论指出,十分感谢。
共同进步
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。