├── .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 |

answer-card logo

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) --------------------------------------------------------------------------------