掩码实现答题卡识别及正确率判断
目录
1 掩码
1.1 概念
掩码(Mask)是一种用于指定图像处理操作区域的工具。掩码通常是一个与图像尺寸相同的二值图像,其中像素值为0表示不处理,像素值为255(或1)表示处理。掩码可以用于多种操作,如图像滤波、图像合成、图像分割等。掩码的尺寸必须与图像的尺寸相同。掩码的像素值通常为0或255(或1),但也可以是其他值,具体取决于应用场景。通过使用掩码,可以更精确地控制图像处理操作的范围,从而实现更复杂的效果。
1.2 创建掩码
- mask = np.zeros((height, width), dtype=np.uint8),创建一个全黑的掩码
- (height, width), 高宽
- dtype=np.uint8 ,数据类型
- cv2.rectangle(mask, (x1, y1), (x2, y2), 255, -1),在掩码上绘制矩形
- (x1, y1), (x2, y2)起点和对角线坐标 ,
- 255颜色, -1表全填充
import cv2
import numpy as np
# 创建一个全黑的掩码
mask = np.zeros((300, 300), dtype=np.uint8)
# 在掩码上绘制一个白色矩形
cv2.rectangle(mask, (50, 50), (100, 100), 255, -1)
cv2.imshow('mask',mask)
cv2.waitKey(0)
2 答题卡识别过程
–
2.1 显示、轮廓排序、透视变换函数
# 显示图像的函数
def cv_chow(name, img):
cv2.imshow(name, img)
cv2.waitKey(0)
# 对四个点进行排序,确保顺序为:左上、右上、右下、左下
def order_points(pts):
rect = np.zeros((4, 2), dtype="float32")
s = pts.sum(axis=1) # 计算每个点的x+y值
rect[0] = pts[np.argmin(s)] # 左上角点,x+y最小
rect[2] = pts[np.argmax(s)] # 右下角点,x+y最大
diff = np.diff(pts, axis=1) # 计算每个点的x-y值
rect[1] = pts[np.argmin(diff)] # 右上角点,x-y最小
rect[3] = pts[np.argmax(diff)] # 左下角点,x-y最大
return rect
# 透视变换函数,将图像中的四边形区域变换为矩形
def four_point_transform(img, pts):
rect = order_points(pts) # 对四个点进行排序
(tl, tr, br, bl) = rect # 分别获取左上、右上、右下、左下四个点
# 计算宽度和高度
widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
max_width = max(int(widthA), int(widthB)) # 取最大宽度
heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
max_height = max(int(heightA), int(heightB)) # 取最大高度
# 定义目标矩形的四个角点
dst = np.array([[0, 0], [max_width - 1, 0],
[max_width - 1, max_height - 1], [0, max_height - 1]], dtype="float32")
# 计算透视变换矩阵并进行变换
m = cv2.getPerspectiveTransform(rect, dst)
warped = cv2.warpPerspective(img, m, (max_width, max_height))
return warped
# 对轮廓进行排序,支持从左到右、从右到左、从上到下、从下到上四种排序方式
def sort_contours(cons, 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
boundingBoxes = [cv2.boundingRect(c) for c in cons] # 获取每个轮廓的边界框
# 根据边界框的x或y坐标进行排序
(cons, boundingBoxes) = zip(*sorted(zip(cons, boundingBoxes),
key=lambda b: b[1][i], reverse=reverse))
return cons, boundingBoxes
2.2 图像预处理
通过灰度化、高斯滤波和Canny边缘检测,提取图像中的轮廓。
代码展示:
img = cv2.imread('test_01.png')
cont_img = img.copy() # 复制图像用于绘制轮廓
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 转换为灰度图像
blurred = cv2.GaussianBlur(gray, (5, 5), 0) # 高斯模糊去噪
edg = cv2.Canny(blurred, 75, 200) # 边缘检测
cv_chow('edg', edg) # 显示边缘检测结果
cnts = cv2.findContours(edg.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
cv2.drawContours(cont_img, cnts, -1, (0, 0, 255), 3) # 绘制轮廓
cv_chow('cont_img', cont_img) # 显示绘制轮廓后的图像
运行结果:
2.3 透视变换
通过轮廓面积排序和近似矩形,找到答题卡的外框,并进行透视变换,将答题卡区域变换为矩形。
代码展示:
docCnt=None
cnts = sorted(cnts,key=cv2.contourArea,reverse=True)
for c in cnts:
p = cv2.arcLength(c,True)
approx = cv2.approxPolyDP(c,0.02*p,True)
if len(approx) == 4:
docCnt = approx
break
warp_t = four_point_transform(img,docCnt.reshape(4,2))
warp_new = warp_t.copy()
cv_chow('warp',warp_t)
2.4 二值化处理
将图像转换为二值图像,便于后续处理。
warped = cv2.cvtColor(warp_t,cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(warped,0,255,
cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
cv_chow('thresh',thresh)
运行结果:
2.5 轮廓检测与筛选
检测图像中的轮廓,并筛选出可能是选项的轮廓。
代码展示:
thresh_cnt = thresh.copy()
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[1]
wraped_cont = cv2.drawContours(warp_t, cnts, -1, (0, 0, 255), 1) # 绘制轮廓
cv_chow('wraped_cont', wraped_cont) # 显示绘制轮廓后的图像
# 筛选出可能是选项的轮廓
qus_cnts = []
for c in cnts:
(x, y, w, h) = cv2.boundingRect(c) # 获取轮廓的边界框
ar = w / float(h) # 计算宽高比
if w >= 20 and h >= 20 and 0.9 <= ar <= 1.1: # 过滤掉不符合条件的轮廓
qus_cnts.append(c)
# 对选项轮廓进行排序(从上到下)
qus_cnts = sort_contours(qus_cnts, method='top-to-bottom')[0]
2.6 答案识别和结果显示
通过掩膜和像素统计,识别用户选择的答案,并与正确答案进行比对。在图像上标注正确答案,并计算和显示分数。
代码展示:
# 计算正确答案的数量
corret = 0
for (q, i) in enumerate(np.arange(0, len(qus_cnts), 5)): # 每5个轮廓为一组(一个问题的选项)
cnts = sort_contours(qus_cnts[i:i + 5])[0] # 对每个问题的选项进行排序
bubbled = None
for (j, c) in enumerate(cnts): # 遍历每个选项
mask = np.zeros(thresh.shape, dtype='uint8') # 创建掩膜
cv2.drawContours(mask, [c], -1, 255, -1) # 在掩膜上绘制轮廓
cv_chow('mask', mask) # 显示掩膜
thresh_mask_and = cv2.bitwise_and(thresh, thresh, mask=mask) # 应用掩膜
cv_chow('thresh_mask_and', thresh_mask_and) # 显示掩膜应用后的图像
total = cv2.countNonZero(thresh_mask_and) # 计算非零像素数量
if bubbled is None or total > bubbled[0]: # 找到填充最多的选项
bubbled = (total, j)
color = (0, 0, 255) # 默认颜色为红色
k = answer_key[q] # 获取正确答案
if k == bubbled[1]: # 如果选择的选项是正确答案
color = (0, 255, 0) # 颜色设置为绿色
corret += 1 # 正确答案数量加1
cv2.drawContours(warp_new, [cnts[k]], -1, color, 3) # 绘制正确答案的轮廓
cv_chow('warppeding', warp_new) # 显示绘制后的图像
# 计算并显示分数
score = (corret / 5.0) * 100
print('[INFO] SCORE: {:.2f}%'.format(score))
cv2.putText(warp_new, ' {:.2f}%'.format(score), (10, 30),
cv2.FONT_HERSHEY_SCRIPT_SIMPLEX, 0.9, (0, 0, 255), 2)
# 显示原始图像和处理后的图像
cv2.imshow('IMG', img)
cv2.imshow('exam', warp_new)
cv2.waitKey(0)
3 代码测试
代码展示:
import numpy as np
import cv2
# 显示图像的函数
def cv_chow(name, img):
cv2.imshow(name, img)
cv2.waitKey(0)
# 对四个点进行排序,确保顺序为:左上、右上、右下、左下
def order_points(pts):
rect = np.zeros((4, 2), dtype="float32")
s = pts.sum(axis=1) # 计算每个点的x+y值
rect[0] = pts[np.argmin(s)] # 左上角点,x+y最小
rect[2] = pts[np.argmax(s)] # 右下角点,x+y最大
diff = np.diff(pts, axis=1) # 计算每个点的x-y值
rect[1] = pts[np.argmin(diff)] # 右上角点,x-y最小
rect[3] = pts[np.argmax(diff)] # 左下角点,x-y最大
return rect
# 透视变换函数,将图像中的四边形区域变换为矩形
def four_point_transform(img, pts):
rect = order_points(pts) # 对四个点进行排序
(tl, tr, br, bl) = rect # 分别获取左上、右上、右下、左下四个点
# 计算宽度和高度
widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
max_width = max(int(widthA), int(widthB)) # 取最大宽度
heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
max_height = max(int(heightA), int(heightB)) # 取最大高度
# 定义目标矩形的四个角点
dst = np.array([[0, 0], [max_width - 1, 0],
[max_width - 1, max_height - 1], [0, max_height - 1]], dtype="float32")
# 计算透视变换矩阵并进行变换
m = cv2.getPerspectiveTransform(rect, dst)
warped = cv2.warpPerspective(img, m, (max_width, max_height))
return warped
# 对轮廓进行排序,支持从左到右、从右到左、从上到下、从下到上四种排序方式
def sort_contours(cons, 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
boundingBoxes = [cv2.boundingRect(c) for c in cons] # 获取每个轮廓的边界框
# 根据边界框的x或y坐标进行排序
(cons, boundingBoxes) = zip(*sorted(zip(cons, boundingBoxes),
key=lambda b: b[1][i], reverse=reverse))
return cons, boundingBoxes
# 定义答案键,表示每个问题的正确答案
answer_key = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1}
# 读取图像
img = cv2.imread('test_01.png')
cont_img = img.copy() # 复制图像用于绘制轮廓
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 转换为灰度图像
blurred = cv2.GaussianBlur(gray, (5, 5), 0) # 高斯模糊去噪
edg = cv2.Canny(blurred, 75, 200) # 边缘检测
cv_chow('edg', edg) # 显示边缘检测结果
# 查找轮廓
cnts = cv2.findContours(edg.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
cv2.drawContours(cont_img, cnts, -1, (0, 0, 255), 3) # 绘制轮廓
cv_chow('cont_img', cont_img) # 显示绘制轮廓后的图像
# 找到最大的四边形轮廓(假设为答题卡的外框)
docCnt = None
cnts = sorted(cnts, key=cv2.contourArea, reverse=True) # 按轮廓面积从大到小排序
for c in cnts:
p = cv2.arcLength(c, True) # 计算轮廓周长
approx = cv2.approxPolyDP(c, 0.02 * p, True) # 多边形逼近
if len(approx) == 4: # 如果是四边形
docCnt = approx
break
# 对图像进行透视变换,将答题卡区域变换为矩形
warp_t = four_point_transform(img, docCnt.reshape(4, 2))
warp_new = warp_t.copy() # 复制变换后的图像
cv_chow('warp', warp_t) # 显示透视变换后的图像
# 对变换后的图像进行二值化处理
warped = cv2.cvtColor(warp_t, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(warped, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
cv_chow('thresh', thresh) # 显示二值化图像
# 查找二值化图像中的轮廓
thresh_cnt = thresh.copy()
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[1]
wraped_cont = cv2.drawContours(warp_t, cnts, -1, (0, 0, 255), 1) # 绘制轮廓
cv_chow('wraped_cont', wraped_cont) # 显示绘制轮廓后的图像
# 筛选出可能是选项的轮廓
qus_cnts = []
for c in cnts:
(x, y, w, h) = cv2.boundingRect(c) # 获取轮廓的边界框
ar = w / float(h) # 计算宽高比
if w >= 20 and h >= 20 and 0.9 <= ar <= 1.1: # 过滤掉不符合条件的轮廓
qus_cnts.append(c)
# 对选项轮廓进行排序(从上到下)
qus_cnts = sort_contours(qus_cnts, method='top-to-bottom')[0]
# 计算正确答案的数量
corret = 0
for (q, i) in enumerate(np.arange(0, len(qus_cnts), 5)): # 每5个轮廓为一组(一个问题的选项)
cnts = sort_contours(qus_cnts[i:i + 5])[0] # 对每个问题的选项进行排序
bubbled = None
for (j, c) in enumerate(cnts): # 遍历每个选项
mask = np.zeros(thresh.shape, dtype='uint8') # 创建掩膜
cv2.drawContours(mask, [c], -1, 255, -1) # 在掩膜上绘制轮廓
cv_chow('mask', mask) # 显示掩膜
thresh_mask_and = cv2.bitwise_and(thresh, thresh, mask=mask) # 应用掩膜
cv_chow('thresh_mask_and
运行结果: