使用 OpenCV-Python 識別答題卡判卷

專注的阿熊發表於2021-12-22

import cv2 as cv

import numpy as np

def cvshow(name, img):

     cv.imshow(name, img)

     cv.waitKey(0)

     cv.destroyAllWindows()

def four_point_transform(img, four_points):

     rect = order_points(four_points)

     (tl, tr, br, bl) = rect

     # 計算輸入的 w h 的值

     widthA = np.sqrt((tr[0] - tl[0]) ** 2 + (tr[1] - tl[1]) ** 2)

     widthB = np.sqrt((br[0] - bl[0]) ** 2 + (br[1] - bl[1]) ** 2)

     maxWidth = max(int(widthA), int(widthB))

     heightA = np.sqrt((tl[0] - bl[0]) ** 2 + (tl[1] - bl[1]) ** 2)

     heightB = np.sqrt((tr[0] - br[0]) ** 2 + (tr[1] - br[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')

     # 最主要的函式就是 cv2.getPerspectiveTransform(rect, dst) cv2.warpPerspective(image, M, (maxWidth, maxHeight))

     M = cv.getPerspectiveTransform(rect, dst)

     warped = cv.warpPerspective(img, M, (maxWidth, maxHeight))

     return warped

def order_points(points):

     res = np.zeros((4, 2), dtype='float32')

     # 按照從前往後 0 1 2 3 分別表示左上、右上、右下、左下的順序將 points 中的數填入 res

     # 將四個座標 x y 相加,和最大的那個是右下角的座標,最小的那個是左上角的座標

     sum_hang = points.sum(axis=1)

     res[0] = points[np.argmin(sum_hang)]

     res[2] = points[np.argmax(sum_hang)]

     # 計算座標 x y 的離散插值 np.diff()

     diff = np.diff(points, axis=1)

     res[1] = points[np.argmin(diff)]

     res[3] = points[np.argmax(diff)]

     # 返回 result

     return res

def sort_contours(contours, method="l2r"):

     # 用於給輪廓排序, l2r, r2l, t2b, b2t

     reverse = False

     i = 0

     if method == "r2l" or method == "b2t":

         reverse = True

     if method == "t2b" or method == "b2t":

         i = 1

     boundingBoxes = [cv.boundingRect(c) for c in contours]

     (contours, boundingBoxes) = zip(*sorted(zip(contours, boundingBoxes), key=lambda a: a[1][i], reverse=reverse))

     return contours, boundingBoxes

# 正確答案

right_key = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1}

# 輸入影像

img = cv.imread('./images/test_01.png')

img_copy = img.copy()

img_gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

cvshow('img-gray', img_gray)

# 影像預處理

# 高斯降噪

img_gaussian = cv.GaussianBlur(img_gray, (5, 5), 1)

cvshow('gaussianblur', img_gaussian)

# canny 邊緣檢測

img_canny = cv.Canny(img_gaussian, 80, 150)

cvshow('canny', img_canny)

# 輪廓識別——答題卡邊緣識別

cnts, hierarchy = cv.findContours(img_canny, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)

cv.drawContours(img_copy, cnts, -1, (0, 0, 255), 3)

cvshow('contours-show', img_copy)

docCnt = None

# 確保檢測到了

if len(cnts) > 0:

     # 根據輪廓大小進行排序

     cnts = sorted(cnts, key=cv.contourArea, reverse=True)

     # 遍歷每一個輪廓

     for c in cnts:

         # 近似

         peri = cv.arcLength(c, True)  # arclength 計算一段曲線的長度或者閉合曲線的周長;

         # 第一個引數輸入一個二維向量,第二個參數列示計算曲線是否閉合

         approx = cv.approxPolyDP(c, 0.02 * peri, True)

         # 用一條頂點較少的曲線 / 多邊形來近似曲線 / 多邊形,以使它們之間的距離 <= 指定的精度;

         # c 是需要近似的曲線, 0.02*peri 是精度的最大值, True 表示曲線是閉合的

         # 準備做透視變換

         if len(approx) == 4:

             docCnt = approx

             break

# 透視變換——提取答題卡主體

docCnt = docCnt.reshape(4, 2)

warped = four_point_transform(img_gray, docCnt)

cvshow('warped', warped)

# 輪廓識別——識別出選項

thresh = cv.threshold(warped, 0, 255, cv.THRESH_BINARY_INV | cv.THRESH_OTSU)[1]

cvshow('thresh', thresh)

thresh_cnts, _ = cv.findContours(thresh, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)

w_copy = warped.copy()

cv.drawContours(w_copy, thresh_cnts, -1, (0, 0, 255), 2)

cvshow('warped_contours', w_copy)

questionCnts = []

# 遍歷,挑出選項的 cnts

for c in thresh_cnts:

     (x, y, w, h) = cv.boundingRect(c)

     ar = w / float(h)

     # 根據實際情況指定標準

     if w >= 20 and h >= 20 and ar >= 0.9 and ar <= 1.1:

         questionCnts.append(c)

# 檢查是否挑出了選項

w_copy2 = warped.copy()

cv.drawContours(w_copy2, questionCnts, -1, (0, 0, 255), 2)

cvshow('questionCnts', w_copy2)

# 檢測每一行選擇的是哪一項,並將結果儲存在元組 bubble 中,記錄正確的個數 correct

# 按照從上到下 t2b 對輪廓進行排序

questionCnts = sort_contours(questionCnts, method="t2b")[0]

correct = 0

# 每行有 5 個選項

for (i, q) in enumerate(np.arange(0, len(questionCnts), 5)):

     # 排序

     cnts = sort_contours(questionCnts[q:q+5])[0]

     bubble = None

     # 得到每一個選項的 mask 並填充,與正確答案進行按位與操作獲得重合點數

     for (j, c) in enumerate(cnts):

         mask = 外匯跟單gendan5.comnp.zeros(thresh.shape, dtype='uint8')

         cv.drawContours(mask, [c], -1, 255, -1)

         cvshow('mask', mask)

         # 透過按位與操作得到 thresh mask 重合部分的畫素數量

         bitand = cv.bitwise_and(thresh, thresh, mask=mask)

         totalPixel = cv.countNonZero(bitand)

         if bubble is None or bubble[0] < totalPixel:

             bubble = (totalPixel, j)

     k = bubble[1]

     color = (0, 0, 255)

     if k == right_key[i]:

         correct += 1

         color = (0, 255, 0)

     # 繪圖

     cv.drawContours(warped, [cnts[right_key[i]]], -1, color, 3)

     cvshow('final', warped)

# 計算最終得分並在圖中標註

score = (correct / 5.0) * 100

print(f"Score: {score}%")

cv.putText(warped, f"Score: {score}%", (10, 30), cv.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)

cv.imshow("Original", img)

cv.imshow("Exam", warped)

cv.waitKey(0)


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69946337/viewspace-2848931/,如需轉載,請註明出處,否則將追究法律責任。

相關文章