引言
在教育考試場景中,手動批改答題卡效率低下且容易出錯。本文將介紹如何使用Python和OpenCV實現一個答題卡自動識別與評分系統,通過計算機視覺技術完成答題卡的透視校正、選項識別和得分計算。該系統可廣泛應用于學校考試、培訓測評等場景,大幅提升批改效率。
環境準備
- Python 3.7+
- OpenCV(
pip install opencv-python
) - NumPy(
pip install numpy
)
核心功能模塊解析
整個系統分為以下核心步驟:
圖像預處理 → 輪廓檢測 → 透視變換 → 選項定位 → 答案匹配 → 結果輸出
1. 導入依賴庫
import numpy as np
import cv2
numpy
:用于數值計算和數組操作。cv2
:OpenCV庫,提供圖像處理和計算機視覺算法。
2. 輔助函數定義
2.1 坐標點排序函數 order_points
答題卡的四個角點需要按左上→右上→右下→左下的順序排列,否則透視變換會出錯。
該函數通過計算坐標的和與差實現排序:
def order_points(pts):rect = np.zeros((4, 2), dtype="float32")s = pts.sum(axis=1) # 計算(x+y),左上角點(x+y)最小,右下角點最大rect[0] = pts[np.argmin(s)] # 左上rect[2] = pts[np.argmax(s)] # 右下diff = np.diff(pts, axis=1) # 計算(y-x),右上角點(y-x)最小(接近0),左下角點最大rect[1] = pts[np.argmin(diff)] # 右上rect[3] = pts[np.argmax(diff)] # 左下return rect
2.2 透視變換函數 four_point_transform
通過透視變換將傾斜的答題卡校正為正視圖。
關鍵步驟:
- 計算變換前的四個角點(
rect
)。 - 確定變換后的目標尺寸(
maxWidth
和maxHeight
)。 - 生成透視變換矩陣(
M
)并應用變換。
def four_point_transform(image, 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))maxWidth = 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))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
2.3 輪廓排序函數 sort_contours
檢測到的選項輪廓需要按從上到下、從左到右排序,以便逐題匹配答案。
支持多種排序方式(左→右、右→左、上→下、下→上):
def sort_contours(cnts, method="left-to-right"):reverse = Falsei = 0 # 排序依據:0為x軸(左右),1為y軸(上下)if method in ["right-to-left", "bottom-to-top"]:reverse = Trueif method in ["top-to-bottom", "bottom-to-top"]:i = 1# 計算每個輪廓的包圍盒(x,y,w,h)boundingBoxes = [cv2.boundingRect(c) for c in cnts]# 按包圍盒的指定維度排序(x或y坐標)(cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes), key=lambda b: b[1][i], reverse=reverse))return cnts, boundingBoxes
2.4 圖像顯示函數 cv_show
調試時用于顯示中間結果:
def cv_show(name, img):cv2.imshow(name, img)cv2.waitKey(0)cv2.destroyAllWindows()
3. 圖像預處理與輪廓檢測
3.1 讀取圖像并灰度化
image = cv2.imread("images/test_01.png") # 替換為你的答題卡路徑
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 灰度化減少計算量
3.2 高斯模糊去噪
blurred = cv2.GaussianBlur(gray, (5, 5), 0) # 5x5核,σ=0
cv_show('blurred', blurred) # 調試:觀察模糊效果
高斯模糊可消除圖像中的高頻噪聲(如紙張紋理、光照不均),避免后續邊緣檢測出現偽影。
3.3 Canny邊緣檢測
edged = cv2.Canny(blurred, 75, 200) # 閾值75和200
cv_show('edged', edged) # 調試:觀察邊緣輪廓
Canny算法通過梯度計算提取圖像邊緣,參數75
和200
分別為低閾值和高閾值,用于區分強邊緣和弱邊緣。
3.4 輪廓檢測與篩選
cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2] # 獲取外層輪廓
# 按面積降序排序(最大的輪廓通常是答題卡)
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)
# 尋找近似四邊形(答題卡的四個角點)
docCnt = None
for c in cnts:peri = cv2.arcLength(c, True) # 計算輪廓周長approx = cv2.approxPolyDP(c, 0.02 * peri, True) # 多邊形近似(精度0.02倍周長)if len(approx) == 4: # 篩選四邊形docCnt = approxbreak
cv2.findContours
:檢測圖像中的輪廓,RETR_EXTERNAL
表示只檢測外層輪廓。approxPolyDP
:通過道格拉斯-普克算法簡化輪廓,保留關鍵頂點(答題卡的四個角點)。
4. 透視變換校正答題卡
warped_t = four_point_transform(image, docCnt.reshape(4, 2)) # 應用透視變換
cv_show('warped', warped_t) # 調試:觀察校正后的答題卡
通過four_point_transform
函數,傾斜的答題卡被校正為正視圖,便于后續選項定位。
5. 選項區域定位與識別
5.1 二值化處理
warped = cv2.cvtColor(warped_t, cv2.COLOR_BGR2GRAY) # 轉為灰度圖
thresh = cv2.threshold(warped, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1] # Otsu自適應閾值
cv_show('thresh', thresh) # 調試:觀察二值化結果
THRESH_BINARY_INV
:反轉二值化結果(選項填涂區域為白色,背景為黑色)。THRESH_OTSU
:自動計算最佳閾值,適應不同光照條件。
5.2 篩選選項輪廓
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2] # 檢測選項輪廓
questionCnts = []
for c in cnts:(x, y, w, h) = cv2.boundingRect(c)ar = w / float(h) # 計算寬高比# 篩選條件:尺寸足夠大且接近正方形(0.9≤寬高比≤1.1)if w >= 20 and h >= 20 and 0.9 <= ar <= 1.1:questionCnts.append(c)
print(f"檢測到{len(questionCnts)}個選項輪廓")
通過寬高比(接近1)和最小尺寸(避免噪聲)篩選出有效選項輪廓。
5.3 按題目分組排序
questionCnts = sort_contours(questionCnts, method="top-to-bottom")[0] # 按從上到下排序
假設每道題有5個選項,按行分組后逐題處理。
6. 答案匹配與評分
6.1 定義正確答案
ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1} # 鍵為題號,值為正確選項索引(0~4)
根據實際題目修改ANSWER_KEY
,例如第0題正確選項是第1個(索引從0開始)。
6.2 逐題識別答案
correct = 0
for (q, i) in enumerate(np.arange(0, len(questionCnts), 5)): # 每5個選項為一題cnts = sort_contours(questionCnts[i:i+5])[0] # 當前題的5個選項(按左→右排序)bubbled = None # 記錄當前題填涂最深的選項for (j, c) in enumerate(cnts): # 遍歷每個選項# 創建掩膜(僅保留當前選項區域)mask = np.zeros(thresh.shape, dtype="uint8")cv2.drawContours(mask, [c], -1, 255, -1) # -1表示填充輪廓內部# 計算掩膜區域的非零像素數(填涂程度)thresh_mask_and = cv2.bitwise_and(thresh, thresh, mask=mask)total = cv2.countNonZero(thresh_mask_and)# 更新填涂最深的選項if bubbled is None or total > bubbled[0]:bubbled = (total, j)# 匹配正確答案k = ANSWER_KEY[q] # 當前題的正確選項索引if k == bubbled[1]: # 填涂選項與正確答案一致color = (0, 255, 0) # 綠色標記正確correct += 1else:color = (0, 0, 255) # 紅色標記錯誤# 在校正后的圖像上繪制結果cv2.drawContours(warped_t, [cnts[k]], -1, color, 3)
- 掩膜技術:通過
mask
僅保留當前選項的區域,統計該區域的白色像素數(填涂程度),像素數最多的選項即為填涂答案。 - 結果可視化:正確選項用綠色框標記,錯誤選項用紅色框標記。
6.3 計算得分并輸出
score = (correct / len(ANSWER_KEY)) * 100 # 總題數為ANSWER_KEY的長度
print(f"[INFO] 得分: {score:.2f}%")
# 在圖像上顯示得分
cv2.putText(warped_t, f"{score:.2f}%", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)
cv2.imshow("Result", warped_t)
cv2.waitKey(0)
運行結果示例
假設測試圖像test_01.png
是一張5題的答題卡,其中3題正確,2題錯誤,則輸出:
檢測到25個選項輪廓
[INFO] 得分: 60.00%
最終圖像會顯示校正后的答題卡,正確選項為綠色框,錯誤選項為紅色框,并標注得分。
注意事項與改進方向
- 圖像質量:確保答題卡光照均勻、無遮擋,否則可能導致輪廓檢測失敗。
- 答案鍵配置:需根據實際題目修改
ANSWER_KEY
字典。 - 魯棒性優化:可添加輪廓面積過濾、傾斜角度校正等功能,適應更復雜的拍攝場景。
- 多題型支持:當前僅支持單選題,可擴展支持多選題(通過統計多個最高像素數的選項)。