您當前的位置:首頁 > 繪畫

使用OpenCV實現答題卡選擇題掃描功能

作者:由 小糊糊 發表于 繪畫時間:2020-02-24

大家在考試的時候都填圖過答題卡,使用答題卡可以使用計算機來自動判斷選擇題答案正確與否,加快閱卷速度。今天我將使用OpenCV來讓計算機自動評閱答題卡。

我們使用的答題卡如下所示,總共有5道題目,每個題目有ABCDE5個選項。

使用OpenCV實現答題卡選擇題掃描功能

要實現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為每個題目正確的答案,先將圖片轉換為灰度圖,然後高斯模糊,再邊緣檢測後的結果如下:

使用OpenCV實現答題卡選擇題掃描功能

# 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

對邊緣檢測後的圖片提取輪廓,按面積從大到小排序,對提取的輪廓使用多邊形近似,如果近似多邊形為四邊形,則說明檢測到答題卡。結果如下:

使用OpenCV實現答題卡選擇題掃描功能

使用透視變換將答題卡變換到正常視角

# 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

))

結果如下,可以看到答題卡被轉換到了俯視視角

使用OpenCV實現答題卡選擇題掃描功能

檢測到答題卡中各個選項的位置

# 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二值化方法將透視變換後的圖片二值化,結果如下:

使用OpenCV實現答題卡選擇題掃描功能

# 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 再次進行輪廓提取,對每個輪廓求取最小外接矩形,若外接矩形滿足一定條件則我們認為該輪廓位置為選項。這樣我們就得到了答題卡中各個選項的輪廓,結果如下:

使用OpenCV實現答題卡選擇題掃描功能

將答題卡中的題目排序

# 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

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

“Original”

image

cv2

imshow

“Exam”

paper

cv2

waitKey

0

正確選項用綠色標記,錯誤選項用紅色標記,結果如下。

使用OpenCV實現答題卡選擇題掃描功能

完整程式碼

# 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

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

“Original”

image

cv2

imshow

“Exam”

paper

cv2

waitKey

0

總結

這是單選答題卡的情況,對於多選答題卡和空白選項答題卡可以在

檢測每題標記塗黑的答案

步驟增加一個閾值,非0畫素個數超過所設閾值就認為是標記選項。

參考

標簽: cv2  Image  contours  答題卡  cnts