使用OpenCV實現答題卡選擇題掃描功能
大家在考試的時候都填圖過答題卡,使用答題卡可以使用計算機來自動判斷選擇題答案正確與否,加快閱卷速度。今天我將使用OpenCV來讓計算機自動評閱答題卡。
我們使用的答題卡如下所示,總共有5道題目,每個題目有ABCDE5個選項。
要實現OpenCV來掃描答題卡答案,我們需要分以下6個步驟完成:
檢測圖片中的答題卡位置。
使用透視變換將答題卡變換到正常視角。
檢測到答題卡中各個選項的位置。
將答題卡中的題目排序。
檢測每題標記塗黑的答案。
判斷答案是否正確。
檢測圖片中的答題卡位置
from
imutils。perspective
import
four_point_transform
from
imutils
import
contours
import
numpy
as
np
import
argparse
import
imutils
import
cv2
# construct the argument parse and parse the arguments
ap
=
argparse
。
ArgumentParser
()
ap
。
add_argument
(
“-i”
,
“——image”
,
required
=
True
,
help
=
“path to the input image”
)
args
=
vars
(
ap
。
parse_args
())
# define the answer key which maps the question number
# to the correct answer
ANSWER_KEY
=
{
0
:
1
,
1
:
4
,
2
:
0
,
3
:
3
,
4
:
1
}
# load the image, convert it to grayscale, blur it
# slightly, then find edges
image
=
cv2
。
imread
(
args
[
“image”
])
gray
=
cv2
。
cvtColor
(
image
,
cv2
。
COLOR_BGR2GRAY
)
blurred
=
cv2
。
GaussianBlur
(
gray
,
(
5
,
5
),
0
)
edged
=
cv2
。
Canny
(
blurred
,
75
,
200
)
cv2
。
imshow
(
“edged”
,
edged
)
cv2
。
waitKey
(
0
)
先匯入必要的包,ANSWER_KEY為每個題目正確的答案,先將圖片轉換為灰度圖,然後高斯模糊,再邊緣檢測後的結果如下:
# find contours in the edge map, then initialize
# the contour that corresponds to the document
cnts
=
cv2
。
findContours
(
edged
。
copy
(),
cv2
。
RETR_EXTERNAL
,
cv2
。
CHAIN_APPROX_SIMPLE
)
cnts
=
imutils
。
grab_contours
(
cnts
)
docCnt
=
None
# ensure that at least one contour was found
if
len
(
cnts
)
>
0
:
# sort the contours according to their size in
# descending order
cnts
=
sorted
(
cnts
,
key
=
cv2
。
contourArea
,
reverse
=
True
)
# loop over the sorted contours
for
c
in
cnts
:
# approximate the contour
peri
=
cv2
。
arcLength
(
c
,
True
)
approx
=
cv2
。
approxPolyDP
(
c
,
0。02
*
peri
,
True
)
# if our approximated contour has four points,
# then we can assume we have found the paper
if
len
(
approx
)
==
4
:
docCnt
=
approx
break
對邊緣檢測後的圖片提取輪廓,按面積從大到小排序,對提取的輪廓使用多邊形近似,如果近似多邊形為四邊形,則說明檢測到答題卡。結果如下:
使用透視變換將答題卡變換到正常視角
# apply a four point perspective transform to both the
# original image and grayscale image to obtain a top-down
# birds eye view of the paper
paper
=
four_point_transform
(
image
,
docCnt
。
reshape
(
4
,
2
))
warped
=
four_point_transform
(
gray
,
docCnt
。
reshape
(
4
,
2
))
結果如下,可以看到答題卡被轉換到了俯視視角
檢測到答題卡中各個選項的位置
# apply Otsu‘s thresholding method to binarize the warped
# piece of paper
thresh
=
cv2
。
threshold
(
warped
,
0
,
255
,
cv2
。
THRESH_BINARY_INV
|
cv2
。
THRESH_OTSU
)[
1
]
先用 Otsu二值化方法將透視變換後的圖片二值化,結果如下:
# find contours in the thresholded image, then initialize
# the list of contours that correspond to questions
cnts
=
cv2
。
findContours
(
thresh
。
copy
(),
cv2
。
RETR_EXTERNAL
,
cv2
。
CHAIN_APPROX_SIMPLE
)
cnts
=
imutils
。
grab_contours
(
cnts
)
questionCnts
=
[]
# loop over the contours
for
c
in
cnts
:
# compute the bounding box of the contour, then use the
# bounding box to derive the aspect ratio
(
x
,
y
,
w
,
h
)
=
cv2
。
boundingRect
(
c
)
ar
=
w
/
float
(
h
)
# in order to label the contour as a question, region
# should be sufficiently wide, sufficiently tall, and
# have an aspect ratio approximately equal to 1
if
w
>=
20
and
h
>=
20
and
ar
>=
0。9
and
ar
<=
1。1
:
questionCnts
。
append
(
c
)
然後我們可以對二值化後的影象thresh 再次進行輪廓提取,對每個輪廓求取最小外接矩形,若外接矩形滿足一定條件則我們認為該輪廓位置為選項。這樣我們就得到了答題卡中各個選項的輪廓,結果如下:
將答題卡中的題目排序
# sort the question contours top-to-bottom, then initialize
# the total number of correct answers
questionCnts
=
contours
。
sort_contours
(
questionCnts
,
method
=
“top-to-bottom”
)[
0
]
correct
=
0
檢測每題標記塗黑的答案
# each question has 5 possible answers, to loop over the
# question in batches of 5
for
(
q
,
i
)
in
enumerate
(
np
。
arange
(
0
,
len
(
questionCnts
),
5
)):
# sort the contours for the current question from
# left to right, then initialize the index of the
# bubbled answer
cnts
=
contours
。
sort_contours
(
questionCnts
[
i
:
i
+
5
])[
0
]
bubbled
=
None
# loop over the sorted contours
for
(
j
,
c
)
in
enumerate
(
cnts
):
# construct a mask that reveals only the current
# “bubble” for the question
mask
=
np
。
zeros
(
thresh
。
shape
,
dtype
=
“uint8”
)
cv2
。
drawContours
(
mask
,
[
c
],
-
1
,
255
,
-
1
)
# apply the mask to the thresholded image, then
# count the number of non-zero pixels in the
# bubble area
mask
=
cv2
。
bitwise_and
(
thresh
,
thresh
,
mask
=
mask
)
total
=
cv2
。
countNonZero
(
mask
)
# if the current total has a larger number of total
# non-zero pixels, then we are examining the currently
# bubbled-in answer
if
bubbled
is
None
or
total
>
bubbled
[
0
]:
bubbled
=
(
total
,
j
)
透過統計二值影象中每個選項輪廓的非0畫素個數,非0畫素個數最多的選項為標記選項。
判斷答案是否正確並計算分數
# initialize the contour color and the index of the
# *correct* answer
color
=
(
0
,
0
,
255
)
k
=
ANSWER_KEY
[
q
]
# check to see if the bubbled answer is correct
if
k
==
bubbled
[
1
]:
color
=
(
0
,
255
,
0
)
correct
+=
1
# draw the outline of the correct answer on the test
cv2
。
drawContours
(
paper
,
[
cnts
[
k
]],
-
1
,
color
,
3
)
# grab the test taker
score
=
(
correct
/
5。0
)
*
100
(
“[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
(
“Original”
,
image
)
cv2
。
imshow
(
“Exam”
,
paper
)
cv2
。
waitKey
(
0
)
正確選項用綠色標記,錯誤選項用紅色標記,結果如下。
完整程式碼
# USAGE
# python test_grader。py ——image images/test_01。png
# import the necessary packages
from
imutils。perspective
import
four_point_transform
from
imutils
import
contours
import
numpy
as
np
import
argparse
import
imutils
import
cv2
# construct the argument parse and parse the arguments
ap
=
argparse
。
ArgumentParser
()
ap
。
add_argument
(
“-i”
,
“——image”
,
required
=
True
,
help
=
“path to the input image”
)
args
=
vars
(
ap
。
parse_args
())
# define the answer key which maps the question number
# to the correct answer
ANSWER_KEY
=
{
0
:
1
,
1
:
4
,
2
:
0
,
3
:
3
,
4
:
1
}
# load the image, convert it to grayscale, blur it
# slightly, then find edges
image
=
cv2
。
imread
(
args
[
“image”
])
gray
=
cv2
。
cvtColor
(
image
,
cv2
。
COLOR_BGR2GRAY
)
blurred
=
cv2
。
GaussianBlur
(
gray
,
(
5
,
5
),
0
)
edged
=
cv2
。
Canny
(
blurred
,
75
,
200
)
# find contours in the edge map, then initialize
# the contour that corresponds to the document
cnts
=
cv2
。
findContours
(
edged
。
copy
(),
cv2
。
RETR_EXTERNAL
,
cv2
。
CHAIN_APPROX_SIMPLE
)
cnts
=
imutils
。
grab_contours
(
cnts
)
docCnt
=
None
# ensure that at least one contour was found
if
len
(
cnts
)
>
0
:
# sort the contours according to their size in
# descending order
cnts
=
sorted
(
cnts
,
key
=
cv2
。
contourArea
,
reverse
=
True
)
# loop over the sorted contours
for
c
in
cnts
:
# approximate the contour
peri
=
cv2
。
arcLength
(
c
,
True
)
approx
=
cv2
。
approxPolyDP
(
c
,
0。02
*
peri
,
True
)
# if our approximated contour has four points,
# then we can assume we have found the paper
if
len
(
approx
)
==
4
:
docCnt
=
approx
break
# cv2。drawContours(image,[docCnt],0,(0,255,0),3)
# cv2。imshow(“contour”,image)
# cv2。waitKey(0)
# apply a four point perspective transform to both the
# original image and grayscale image to obtain a top-down
# birds eye view of the paper
paper
=
four_point_transform
(
image
,
docCnt
。
reshape
(
4
,
2
))
warped
=
four_point_transform
(
gray
,
docCnt
。
reshape
(
4
,
2
))
# cv2。imshow(“paper”,paper)
# cv2。imshow(“warped”,warped)
# cv2。waitKey(0)
# apply Otsu’s thresholding method to binarize the warped
# piece of paper
thresh
=
cv2
。
threshold
(
warped
,
0
,
255
,
cv2
。
THRESH_BINARY_INV
|
cv2
。
THRESH_OTSU
)[
1
]
# cv2。imshow(“thresh”,thresh)
# cv2。waitKey(0)
# find contours in the thresholded image, then initialize
# the list of contours that correspond to questions
cnts
=
cv2
。
findContours
(
thresh
。
copy
(),
cv2
。
RETR_EXTERNAL
,
cv2
。
CHAIN_APPROX_SIMPLE
)
cnts
=
imutils
。
grab_contours
(
cnts
)
questionCnts
=
[]
# loop over the contours
for
c
in
cnts
:
# compute the bounding box of the contour, then use the
# bounding box to derive the aspect ratio
(
x
,
y
,
w
,
h
)
=
cv2
。
boundingRect
(
c
)
ar
=
w
/
float
(
h
)
# in order to label the contour as a question, region
# should be sufficiently wide, sufficiently tall, and
# have an aspect ratio approximately equal to 1
if
w
>=
20
and
h
>=
20
and
ar
>=
0。9
and
ar
<=
1。1
:
questionCnts
。
append
(
c
)
# cv2。drawContours(paper,questionCnts,-1,(0,255,255),2)
# cv2。imshow(“questionCnts”,paper)
# cv2。waitKey(0)
# sort the question contours top-to-bottom, then initialize
# the total number of correct answers
questionCnts
=
contours
。
sort_contours
(
questionCnts
,
method
=
“top-to-bottom”
)[
0
]
correct
=
0
# each question has 5 possible answers, to loop over the
# question in batches of 5
for
(
q
,
i
)
in
enumerate
(
np
。
arange
(
0
,
len
(
questionCnts
),
5
)):
# sort the contours for the current question from
# left to right, then initialize the index of the
# bubbled answer
cnts
=
contours
。
sort_contours
(
questionCnts
[
i
:
i
+
5
])[
0
]
bubbled
=
None
# loop over the sorted contours
for
(
j
,
c
)
in
enumerate
(
cnts
):
# construct a mask that reveals only the current
# “bubble” for the question
mask
=
np
。
zeros
(
thresh
。
shape
,
dtype
=
“uint8”
)
cv2
。
drawContours
(
mask
,
[
c
],
-
1
,
255
,
-
1
)
# apply the mask to the thresholded image, then
# count the number of non-zero pixels in the
# bubble area
mask
=
cv2
。
bitwise_and
(
thresh
,
thresh
,
mask
=
mask
)
total
=
cv2
。
countNonZero
(
mask
)
# if the current total has a larger number of total
# non-zero pixels, then we are examining the currently
# bubbled-in answer
if
bubbled
is
None
or
total
>
bubbled
[
0
]:
bubbled
=
(
total
,
j
)
# initialize the contour color and the index of the
# *correct* answer
color
=
(
0
,
0
,
255
)
k
=
ANSWER_KEY
[
q
]
# check to see if the bubbled answer is correct
if
k
==
bubbled
[
1
]:
color
=
(
0
,
255
,
0
)
correct
+=
1
# draw the outline of the correct answer on the test
cv2
。
drawContours
(
paper
,
[
cnts
[
k
]],
-
1
,
color
,
3
)
# grab the test taker
score
=
(
correct
/
5。0
)
*
100
(
“[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
(
“Original”
,
image
)
cv2
。
imshow
(
“Exam”
,
paper
)
cv2
。
waitKey
(
0
)
總結
這是單選答題卡的情況,對於多選答題卡和空白選項答題卡可以在
檢測每題標記塗黑的答案
步驟增加一個閾值,非0畫素個數超過所設閾值就認為是標記選項。
參考
上一篇:吃蘋果真的有醒酒的功效嗎?
下一篇:如何裝做很懂乃木坂46?