OpenCV-简易答题卡识别

参考自:https://www.pyimagesearch.com/2016/10/03/bubble-sheet-multiple-choice-scanner-and-test-grader-using-omr-python-and-opencv/

一个简易的答题卡识别与分数判断小程序

修改说明:

1.不import imutils库,直接找mutils的源码,复制需要的函数的源码过来,分析算法原理

2.在jupter notebook中测试,可以方便地分阶段测试

**引入必要的库**

import numpy as np
import cv2

import matplotlib
import matplotlib.pyplot as plt
# Allow image embeding in notebook
%matplotlib inline

定义需要的函数

4边形4点排序函数

# ----------------------------------------------------------------------
# 【4边形4点排序函数】
#     输入:4边形任意顺序的4个顶点
#     输出:按照一定顺序的4个顶点
# https://github.com/jrosebr1/imutils/blob/master/imutils/perspective.py
# ----------------------------------------------------------------------
def order_points(pts):
    rect = np.zeros((4, 2), dtype = "float32")# 按照左上、右上、右下、左下顺序初始化坐标

    s = pts.sum(axis = 1)# 计算点xy的和
    rect[0] = pts[np.argmin(s)]# 左上角的点的和最小
    rect[2] = pts[np.argmax(s)]# 右下角的点的和最大

    diff = np.diff(pts, axis = 1)# 计算点xy之间的差
    rect[1] = pts[np.argmin(diff)]# 右上角的差最小
    rect[3] = pts[np.argmax(diff)]# 左下角的差最小

    return rect# 返回4个顶点的顺序

4点变换函数

# ----------------------------------------------------------------------
# 【4点变换函数】
#      输入:原始图像+4个顶点
#      输出:变换后的图像
# https://github.com/jrosebr1/imutils/blob/master/imutils/perspective.py
# ----------------------------------------------------------------------
def four_point_transform(image, pts):
    rect = order_points(pts)# 获得一致的顺序的点并分别解包他们
    (tl, tr, br, bl) = rect

    # 计算新图像的宽度(x)
    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))#右上和左上之间距离
    maxWidth = max(int(widthA), int(widthB))# 取大者

    # 计算新图像的高度(y)
    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))#左上和左下之间距离
    maxHeight = max(int(heightA), int(heightB))

    # 有了新图像的尺寸, 构造透视变换后的顶点集合
    dst = np.array([
        [0, 0], # -------------------------左上
        [maxWidth - 1, 0], # --------------右上
        [maxWidth - 1, maxHeight - 1], # --右下
        [0, maxHeight - 1]], # ------------左下
            dtype = "float32")

    M = cv2.getPerspectiveTransform(rect, dst)# 计算透视变换矩阵
    warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight)) # 执行透视变换

    return warped #返回透视变换后的图像

轮廓排序函数

# --------------------------------------------------------------------
# 【轮廓排序函数】
#      输入:轮廓,排序方式
#      输出:排序好的轮廓
#  https://github.com/jrosebr1/imutils/blob/master/imutils/contours.py
# --------------------------------------------------------------------
def sort_contours(cnts, method="left-to-right"):
    # 初始化逆序标志和排序索引
    reverse = False
    i = 0

    # 是否需逆序处理
    if method == "right-to-left" or method == "bottom-to-top":
        reverse = True

    # 是否需要按照y坐标函数
    if method == "top-to-bottom" or method == "bottom-to-top":
        i = 1

    # 构造包围框列表,并从上到下对它们进行排序
    boundingBoxes = [cv2.boundingRect(c) for c in cnts]
    (cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes),key=lambda b: b[1][i], reverse=reverse))

    # 返回已排序的轮廓线和边框列表
    return cnts, boundingBoxes

图像识别部分

读入图片+预处理

# 【1】读入图片+预处理
image = cv2.imread('omr_test_01.png')# 加载图片
#cv2.imshow("Original", image)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)# 转灰度
blurred = cv2.GaussianBlur(gray, (5, 5), 0)# 高斯模糊
edged = cv2.Canny(blurred, 75, 200)# 边缘检测
fig = plt.figure(figsize=(15, 10))
plt.subplot(1, 2, 1)
plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))#plt显示是RGB顺序
plt.axis('off')
plt.subplot(1, 2, 2)
plt.imshow(edged,cmap ='gray')
plt.axis('off')
#cv2.imshow("edged", edged)
#cv2.waitKey()

OpenCV-简易答题卡识别

检测到图片中的答题卡

# 【2】检测到图片中的答题卡(python2 用:cnts,_ )
_,cnts,_ = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)# 从边缘图中寻找轮廓
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】应用透视变换来提取图中的答题卡
paper = four_point_transform(image, docCnt.reshape(4, 2))# 对原始图进行四点透视变换
warped = four_point_transform(gray, docCnt.reshape(4, 2))# 对灰度图进行四点透视变换
#cv2.imshow("warped", warped)# 透视变换图
#cv2.waitKey()
fig = plt.figure(figsize=(8, 8))
plt.imshow(warped,cmap ='gray')
plt.axis('off')

OpenCV-简易答题卡识别

提取气泡/圆点

# 【4】从透视变换后的答题卡中提取气泡/圆点
thresh = cv2.threshold(warped, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]# OTSU二值化
_,cnts,_ = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)# 在二值图像中查找轮廓
questionCnts = [] # 初始化气泡轮廓

# 对每一个轮廓进行循环处理
for c in cnts:
    (x, y, w, h) = cv2.boundingRect(c) # 计算轮廓的边界框
    ar = w / float(h)# 计算宽高比

    # 轮廓是气泡->边至少是20个像素,且宽高比近似为1
    if w >= 20 and h >= 20 and ar >= 0.9 and ar <= 1.1:
        questionCnts.append(c)# 存储气泡轮廓

答案判断部分

构建答案字典

# 构建答案字典,键为题目号,值为正确答案
ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1}

气泡排序

# 【5】将题目/气泡排序成行
questionCnts = sort_contours(questionCnts, method="top-to-bottom")[0]# 从顶部到底部将气泡轮廓排序
correct = 0 # 初始化正确答案数的变量

循环判断

# 每个题目有5个选项,所以5个气泡一组循环处理
fig = plt.figure(figsize=(15,15))
n = 1
for (q, i) in enumerate(np.arange(0, len(questionCnts), 5)):
    cnts = sort_contours(questionCnts[i:i + 5])[0]# 从左到右为当前题目的气泡轮廓排序
    bubbled = None # 初始化被涂画的气泡变量
# 【6】判断每行中被标记/涂的答案
    for (j, c) in enumerate(cnts):# 对一行从左到右排列好的气泡轮廓进行遍历
        mask = np.zeros(thresh.shape, dtype="uint8")# 构造只有当前气泡轮廓区域的掩模图像
        cv2.drawContours(mask, [c], -1, 255, -1)

        mask = cv2.bitwise_and(thresh, thresh, mask=mask)# 对二值图像应用掩模图像
        total = cv2.countNonZero(mask)# 计算气泡区域内的非零像素点

        #cv2.imshow("mask", mask)
        #cv2.waitKey(100)
        plt.subplot(5, 5, n)  # 5 rows, 5 per row
        plt.axis('off')
        n += 1
        plt.imshow(mask,cmap ='gray')

        if bubbled is None or total > bubbled[0]:# 如果像素点数最大
            bubbled = (total, j) # 同气泡选项序号一起记录下来

    color = (0, 0, 255) # 初始化轮廓颜色为红色
    k = ANSWER_KEY[q] # 获取正确答案序号

    # 【7】在我答案字典中查找正确的答案来判断答题是否正确 
    if k == bubbled[1]: # 检查由填充气泡获得的答案是否正确
        color = (0, 255, 0)# 正确则将轮廓颜色设置为绿色
        correct += 1

    # 画出正确答案的轮廓线。
    cv2.drawContours(paper, [cnts[k]], -1, color, 3)
#cv2.waitKey()

OpenCV-简易答题卡识别

计算分数并打分

# 【8】计算分数并打分

score = (correct / 5.0) * 100
print("[INFO] score: {:.2f}%".format(score))
cv2.putText(paper, "{:.2f}%".format(score), (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)
#cv2.imshow("Exam", paper)
#cv2.waitKey(0)
fig = plt.figure(figsize=(8, 8))
plt.imshow(cv2.cvtColor(paper, cv2.COLOR_BGR2RGB))
plt.axis('off')

[INFO] score: 80.00%

OpenCV-简易答题卡识别

猜你喜欢

转载自blog.51cto.com/15060517/2641111