├── .gitignore
├── images
├── test_01.jpg
├── test_01.png
├── test_02.png
├── test_03.png
├── test_04.png
├── test_05.png
├── answer-card.jpg
├── example_test.png
└── result
│ └── test_01_result.png
├── .vscode
├── settings.json
└── launch.json
├── requirements.txt
├── README.md
└── get_answer.py
/.gitignore:
--------------------------------------------------------------------------------
1 | env350
--------------------------------------------------------------------------------
/images/test_01.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yt46767/answer-card/HEAD/images/test_01.jpg
--------------------------------------------------------------------------------
/images/test_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yt46767/answer-card/HEAD/images/test_01.png
--------------------------------------------------------------------------------
/images/test_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yt46767/answer-card/HEAD/images/test_02.png
--------------------------------------------------------------------------------
/images/test_03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yt46767/answer-card/HEAD/images/test_03.png
--------------------------------------------------------------------------------
/images/test_04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yt46767/answer-card/HEAD/images/test_04.png
--------------------------------------------------------------------------------
/images/test_05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yt46767/answer-card/HEAD/images/test_05.png
--------------------------------------------------------------------------------
/images/answer-card.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yt46767/answer-card/HEAD/images/answer-card.jpg
--------------------------------------------------------------------------------
/images/example_test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yt46767/answer-card/HEAD/images/example_test.png
--------------------------------------------------------------------------------
/images/result/test_01_result.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yt46767/answer-card/HEAD/images/result/test_01_result.png
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.pythonPath": "env350\\Scripts\\python.exe",
3 | "python.linting.pylintEnabled": true,
4 | "python.linting.enabled": true
5 | }
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | brotli==1.0.7
2 | cryptography==2.8
3 | Cython==0.29.14
4 | importlib_metadata==0.23
5 | imutils==0.5.3
6 | ipaddr==2.2.0
7 | keyring==19.2.0
8 | lockfile==0.12.2
9 | lxml==4.4.1
10 | mock==3.0.5
11 | mypy_extensions==0.4.3
12 | numpy==1.17.4
13 | ordereddict==1.1
14 | protobuf==3.10.0
15 | pyOpenSSL==19.1.0
16 | simplejson==3.17.0
17 | toml==0.10.0
18 | usercustomize==1.0.0
19 | wincertstore==0.2
20 | zipp==0.6.0
21 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "答题卡",
9 | "type": "python",
10 | "request": "launch",
11 | "program": "${workspaceRoot}/get_answer.py",
12 | "console": "integratedTerminal",
13 | "args": [
14 | "-i",
15 | "./images/test_01.jpg"
16 | ]
17 | }
18 | ]
19 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |

2 |
3 | # List of Contents
4 | ```
5 | answer-card
6 | ├─.vscode vscode配置
7 | | ├─launch.json 运行配置文件
8 | | └─settings.json 当前项目的vscode配置
9 | ├─images 存储图像
10 | | ├─result 识别处理后得出的图像
11 | | | └─test_01_result.png test_01.png识别处理后得出的图像
12 | | ├─example_test.png 待识别图像模板
13 | | ├─test_01.jpg 待识别图像
14 | | ├─test_01.png 待识别图像
15 | | ├─test_02.png 待识别图像
16 | | ├─test_03.png 待识别图像
17 | | ├─test_04.png 待识别图像
18 | | └─test_05.png 待识别图像
19 | ├─.gitignore git忽略配置文件
20 | ├─get_answer.py 主程序代码
21 | ├─README.md 项目说明文本
22 | └─requirements.txt python第三方包清单
23 | ```
24 |
25 |
26 |
27 | # Start to Use
28 |
29 | ## 使用python版本号
30 | 此项目使用3.5.0,建议范围:3.5.0 ~ 3.7.0
31 |
32 | ## 创建并激活虚拟环境
33 | ```shell
34 | virtualenv -p C:\Users\ASUS\AppData\Local\Programs\Python\Python35\python.exe env350
35 | env350/scripts/activate
36 | ```
37 | ## 安装依赖包
38 | ```shell
39 | pip install pipreqs
40 | pipreqs ./ --encoding=utf8 --force
41 | pip install -r requirements.txt
42 | pip install opencv-python
43 | ```
44 |
45 | ## 项目运行
46 | + 手动命令运行
47 | ```shell
48 | py ./get_answer.py -i .//images//test_01.png
49 | ```
50 | + Vscode运行
51 | 可以通过.vscode的lauch.json配置运行及调试。
52 |
53 |
54 | # Help and Support
55 | ## opencv的api文档(国内可以访问)
56 | http://www.opencv.org.cn/opencvdoc/2.3.2/html/index.html
57 |
58 | ## python的第三方库
59 | https://pypi.org/
60 |
61 |
62 | # License
63 |
64 | [MIT](http://opensource.org/licenses/MIT)
65 |
66 | Copyright (c) 2019-present, Thomas Yang
67 |
--------------------------------------------------------------------------------
/get_answer.py:
--------------------------------------------------------------------------------
1 | #导入工具包
2 | import numpy as np
3 | import argparse
4 | import imutils
5 | import cv2
6 |
7 | # 设置参数
8 | ap = argparse.ArgumentParser()
9 | ap.add_argument("-i", "--image", required=True,
10 | help="path to the input image")
11 | args = vars(ap.parse_args())
12 |
13 | # 正确答案
14 | ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1} #{0:B, 1:E, 2:A, 3:D, 4:B}
15 |
16 | def order_points(pts):
17 | # 一共4个坐标点
18 | rect = np.zeros((4, 2), dtype = "float32")
19 |
20 | # 按顺序找到对应坐标0123分别是 左上,右上,右下,左下
21 | # 计算左上,右下
22 | s = pts.sum(axis = 1)
23 | rect[0] = pts[np.argmin(s)]
24 | rect[2] = pts[np.argmax(s)]
25 |
26 | # 计算右上和左下
27 | diff = np.diff(pts, axis = 1)
28 | rect[1] = pts[np.argmin(diff)]
29 | rect[3] = pts[np.argmax(diff)]
30 |
31 | return rect
32 |
33 | def four_point_transform(image, pts):
34 | # 获取输入坐标点
35 | rect = order_points(pts)
36 | (tl, tr, br, bl) = rect
37 |
38 | # 计算输入的w和h值
39 | widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
40 | widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
41 | maxWidth = max(int(widthA), int(widthB))
42 |
43 | heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
44 | heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
45 | maxHeight = max(int(heightA), int(heightB))
46 |
47 | # 变换后对应坐标位置
48 | dst = np.array([
49 | [0, 0],
50 | [maxWidth - 1, 0],
51 | [maxWidth - 1, maxHeight - 1],
52 | [0, maxHeight - 1]], dtype = "float32")
53 |
54 | # 计算变换矩阵
55 | M = cv2.getPerspectiveTransform(rect, dst)
56 | warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
57 |
58 | # 返回变换后结果
59 | return warped
60 | def sort_contours(cnts, method="left-to-right"):
61 | reverse = False
62 | i = 0
63 | if method == "right-to-left" or method == "bottom-to-top":
64 | reverse = True
65 | if method == "top-to-bottom" or method == "bottom-to-top":
66 | i = 1
67 | boundingBoxes = [cv2.boundingRect(c) for c in cnts]
68 | (cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes),
69 | key=lambda b: b[1][i], reverse=reverse))
70 | return cnts, boundingBoxes
71 | def cv_show(name,img):
72 | cv2.imshow(name, img)
73 | cv2.waitKey(0)
74 | cv2.destroyAllWindows()
75 |
76 | # 预处理
77 | image = cv2.imread(args["image"]) #imread:从文件加载图像
78 | contours_img = image.copy()
79 | gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
80 | blurred = cv2.GaussianBlur(gray, (5, 5), 0)
81 | cv_show('blurred',blurred)
82 | edged = cv2.Canny(blurred, 75, 200) #Canny:使用[ Canny 86]算法查找图像的边缘
83 | cv_show('edged',edged)
84 |
85 | # 轮廓检测
86 | # CvMemStorage* storage = 0;
87 | # storage = cvCreateMemStorage(0);
88 | cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL,
89 | cv2.CHAIN_APPROX_SIMPLE)[0] #findContours:查找二进制图像中的轮廓。
90 | # cnts = cnts[1] if imutils.is_cv3() else cnts[0] #opencv的findContours兼容性:cv2,则[0];cv3以上,则[1]
91 | cv2.drawContours(contours_img,cnts,-1,(0,0,255),3) #drawContours:绘制轮廓轮廓或填充轮廓。drawContours(目标图像,所有输入轮廓[每个轮廓都存储为点向量],要绘制轮廓的参数[如果为负,则绘制所有轮廓],轮廓的颜色,绘制等高线的粗细[如果它为负数(例如, thickness = CV_FILLED),则绘制轮廓内部])
92 | cv_show('contours_img',contours_img)
93 | docCnt = None
94 |
95 | # 确保检测到了
96 | if len(cnts) > 0:
97 | # 根据轮廓大小进行排序
98 | cnts = sorted(cnts, key=cv2.contourArea, reverse=True)
99 |
100 | # 遍历每一个轮廓
101 | for c in cnts:
102 | # 近似
103 | peri = cv2.arcLength(c, True)
104 | approx = cv2.approxPolyDP(c, 0.02 * peri, True)
105 |
106 | # 准备做透视变换
107 | if len(approx) == 4:
108 | docCnt = approx
109 | break
110 |
111 | # 执行透视变换
112 |
113 | warped = four_point_transform(gray, docCnt.reshape(4, 2))
114 | cv_show('warped',warped)
115 | # Otsu's 阈值处理
116 | thresh = cv2.threshold(warped, 0, 255,
117 | cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
118 | cv_show('thresh',thresh)
119 | thresh_Contours = thresh.copy()
120 | # 找到每一个圆圈轮廓
121 | cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,
122 | cv2.CHAIN_APPROX_SIMPLE)[0]
123 | # cnts = cnts[1] if imutils.is_cv3() else cnts[0]
124 | cv2.drawContours(thresh_Contours,cnts,-1,(0,0,255),3)
125 | cv_show('thresh_Contours',thresh_Contours)
126 | questionCnts = []
127 |
128 | # 遍历
129 | for c in cnts:
130 | # 计算比例和大小
131 | (x, y, w, h) = cv2.boundingRect(c)
132 | ar = w / float(h)
133 |
134 | # 根据实际情况指定标准
135 | if w >= 20 and h >= 20 and ar >= 0.9 and ar <= 1.1:
136 | questionCnts.append(c)
137 |
138 | # 按照从上到下进行排序
139 | questionCnts = sort_contours(questionCnts,
140 | method="top-to-bottom")[0]
141 | correct = 0
142 |
143 | # 每排有5个选项
144 | for (q, i) in enumerate(np.arange(0, len(questionCnts), 5)):
145 | # 排序
146 | cnts = sort_contours(questionCnts[i:i + 5])[0]
147 | bubbled = None
148 |
149 | # 遍历每一个结果
150 | for (j, c) in enumerate(cnts):
151 | # 使用mask来判断结果
152 | mask = np.zeros(thresh.shape, dtype="uint8")
153 | cv2.drawContours(mask, [c], -1, 255, -1) #-1表示填充
154 | cv_show('mask',mask)
155 | # 通过计算非零点数量来算是否选择这个答案
156 | mask = cv2.bitwise_and(thresh, thresh, mask=mask)
157 | total = cv2.countNonZero(mask)
158 |
159 | # 通过阈值判断
160 | if bubbled is None or total > bubbled[0]:
161 | bubbled = (total, j)
162 |
163 | # 对比正确答案
164 | color = (0, 0, 255)
165 | k = ANSWER_KEY[q]
166 |
167 | # 判断正确
168 | if k == bubbled[1]:
169 | color = (0, 255, 0)
170 | correct += 1
171 |
172 | # 绘图
173 | cv2.drawContours(warped, [cnts[k]], -1, color, 3)
174 |
175 |
176 | score = (correct / 5.0) * 100
177 | print("[INFO] score: {:.2f}%".format(score)) #输出答题卡得分
178 | cv2.putText(warped, "{:.2f}%".format(score), (10, 30),
179 | cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)
180 | cv2.imshow("Original", image)
181 | cv2.imshow("Exam", warped)
182 | cv2.waitKey(0)
--------------------------------------------------------------------------------