OpenCV-Easy Answer Sheet Recognition

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

A simple answer sheet recognition and score judgment applet

Modify the description:

1. Do not import the imutils library, directly find the source code of mutils, copy the source code of the required function, and analyze the principle of the algorithm

2. Test in the jupter notebook, you can easily test in stages

**引入必要的库**

import numpy as np
import cv2

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

Define the required function

Quadrilateral 4-point sorting function

# ----------------------------------------------------------------------
# 【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-point transformation function

# ----------------------------------------------------------------------
# 【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 #返回透视变换后的图像

Contour sorting function

# --------------------------------------------------------------------
# 【轮廓排序函数】
#      输入:轮廓,排序方式
#      输出:排序好的轮廓
#  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

Image recognition part

Read in pictures + preprocessing

# 【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-Easy Answer Sheet Recognition

The answer sheet in the picture is detected

# 【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

Perspective transformation to extract answer sheet

# 【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-Easy Answer Sheet Recognition

Extract bubbles/dots

# 【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 judgment part

Build a dictionary of answers

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

Bubble sort

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

Cycle judgment

# 每个题目有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-Easy Answer Sheet Recognition

Calculate and score points

# 【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-Easy Answer Sheet Recognition

Guess you like

Origin blog.51cto.com/15060517/2641111