目录
[(1) 、读取和处理图像](#(1) 、读取和处理图像)
前言
在图像处理领域,Python 的 OpenCV 库为我们提供了强大的工具。今天我们来探讨一段有趣的代码,它能够实现试卷的自动阅卷功能。
步骤解析
1、导入必要的模块
python
import numpy as np
import cv2
2、定义辅助函数
(1)、图像显示函数
用于显示图像,接受图像名称和图像矩阵作为参数,显示图像并等待用户按键
python
def cv_show(name, img):
cv2.imshow(name, img)
cv2.waitKey(0)
(2)、确定答题卡的四个角点坐标,并进行排序函数
对给定的四个点进行排序,以便确定它们分别对应左上、右上、右下和左下的位置
python
def order_points(pts):
# 一共4个坐标点
rect = np.zeros((4, 2), dtype="float32") # 用来存储排序之后的坐标位置
# 按顺序找到对应坐标0123分别是 左上,右上,右下,左下
s = pts.sum(axis=1) # 对pts矩阵的每一行进行求和操作。(x+y)
rect[0] = pts[np.argmin(s)]
rect[2] = pts[np.argmax(s)]
diff = np.diff(pts, axis=1) # 对pts矩阵的每一行进行求差操作。(y-x)
rect[1] = pts[np.argmin(diff)]
rect[3] = pts[np.argmax(diff)]
return rect
(3)、对图像进行透视变换函数
对图像进行透视变换,将一个包含四个点的不规则四边形变换为规则的矩形
python
# 将透视扭曲的矩形变换成一个规则的矩阵
def four_point_transform(image, pts):
# 获取输入坐标点
rect = order_points(pts) # 为上述排序的四个点
(tl, tr, br, bl) = rect # 分别返回给四个值,分别表示为左上、右上、右下、左下
# 计算输入的w和h值
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')
# 图像透视变换 cv2.getPerspectiveTransform(src,dst[,solveMethod])→ M获得转换之间的关系
# cv2.warpPerspective(src, Mp, dsizel, dstl, flagsl, borderModel, borderValue]]1])- dst
# #参数说明:
# src:变换前图像四边形顶点坐标/第2个是原图
# MP:透视变换矩阵,3行3列
# dsize:输出图像的大小,二元元组(width,heiqht)
M = cv2.getPerspectiveTransform(rect, dst) # 根据原始点和变换后的点计算透视变换矩阵M
warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight)) # 对原始图像,针推变换矩阵和输出图像大小进行透视变换,返回变换后的图片
# 返回变换后的结果
return warped
(4)对给定的轮廓列表进行排序函数
此函数可以按照指定的方法(如从左到右、从右到左、从上到下、从下到上)对轮廓进行排序
python
def sort_contours(cnts, method='left-to-right'):
reverse = False
i = 0
if method == "right-to-left" or method == 'bottom-to-top':
reverse = True
if method == 'top-to-bottom' or method == 'bottom-to-top':
i = 1
boundingBoxes = [cv2.boundingRect(c) for c in cnts]
(cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes),
key=lambda b: b[1][i],
reverse=reverse))
return cnts, boundingBoxes
3、主程序流程
(1) 、读取和处理图像
- 读取图像
test_01.png
并复制一份用于后续绘制轮廓。 - 将图像转换为灰度图像。
- 使用高斯模糊对灰度图像进行平滑处理。
- 使用 Canny 边缘检测算法检测图像中的边缘。
- 绘制检测到的边缘轮廓在复制的图像上并显示。
python
ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1} # 定义一个字典,里面存放答题卡正确答案
image = cv2.imread(r'image/test_01.png')
contours_img = image.copy()
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
cv_show('blurred', blurred)
edged = cv2.Canny(blurred, 75, 200)
cv_show('edged', edged)
(2)、寻找答题卡轮廓
- 使用
cv2.findContours
函数找到边缘图像中的所有轮廓。 - 按照轮廓面积从大到小对轮廓进行排序。
- 遍历轮廓,通过轮廓的周长和近似多边形来寻找近似为四边形的轮廓,认为这个四边形是文档的轮廓。
- 对找到的文档轮廓进行透视变换,将文档区域变换为规则的矩形并显示。
python
# 轮廓检测
cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[1]
cv2.drawContours(contours_img, cnts, -1, (0, 0, 255), 3)
cv_show('contours_img', contours_img)
docCnt = None
# 根据轮廓大小进行排序
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)
for c in cnts:
peri = cv2.arcLength(c, True)
approx = cv2.approxPolyDP(c, 0.002 * peri, True)
if len(approx) == 4:
docCnt = approx
break
# 透视变换
warped_t = four_point_transform(image, docCnt.reshape(4, 2))
warped_new = warped_t.copy()
cv_show('warped', warped_t)
(3)、处理变换过的区域
- 将透视变换后的图像转换为灰度图像。
- 使用阈值处理将灰度图像转换为二值图像。
- 再次找到二值图像中的所有轮廓并在变换后的图像上绘制轮廓并显示。
python
warped = cv2.cvtColor(warped_t, cv2.COLOR_BGR2GRAY)
# 阈值处理
thresh = cv2.threshold(warped, 0, 255,
cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
cv_show('thresh', thresh)
thresh_Contours = thresh.copy()
# 找到每一个轮廓
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[1]
warped_Contours = cv2.drawContours(warped_t, cnts, -1, (0, 255, 0), 1)
cv_show('warped_Contours', warped_Contours)
(4)、识别答题区域(答题卡的作答区)
- 遍历二值图像中的轮廓,根据轮廓的宽高比、大小等条件筛选出可能的答题区域轮廓。
- 对每个问题的答题区域轮廓按照从上到下的顺序进行排序。
python
questionCnts = []
# 遍历轮廓并计算比例大小
for c in cnts:
(x, y, w, h) = cv2.boundingRect(c)
ar = w / float(h)
if w >= 20 and h >= 20 and ar >= 0.9 and ar <= 1.1:
questionCnts.append(c)
questionCnts = sort_contours(questionCnts, method="top-to-bottom")[0]
(5)、判断答案的正确性
- 对于每个问题,遍历其五个选项的轮廓,使用掩码和计数非零像素的方法来判断哪个选项被选中。
- 根据预设的答案键来判断答案是否正确,用不同颜色在变换后的图像上绘制正确和错误的答案轮廓并显示。
python
correct = 0
for (q, i) in enumerate(np.arange(0, len(questionCnts), 5)):
cnts = sort_contours(questionCnts[i:i + 5])[0]
bubbled = None
# 遍历每一个结果
for (j, c) in enumerate(cnts):
# 使用mask来判断结果
mask = np.zeros(thresh.shape, dtype="uint8")
cv2.drawContours(mask, [c], -1, 255, -1)
cv_show('mask', mask)
thresh_mask_and = cv2.bitwise_and(thresh, thresh, mask=mask)
cv_show('thresh_mask_and', thresh_mask_and)
total = cv2.countNonZero(thresh_mask_and)
if bubbled is None or total > bubbled[0]:
bubbled = (total, j)
color = (0, 0, 255)
k = ANSWER_KEY[q]
if k == bubbled[1]:
color = (0, 255, 0)
correct += 1
cv2.drawContours(warped_new, [cnts[k]], -1, color, 3)
cv_show("warpeding", warped_new)
(6)、计算得分并在答题卡上显示结果
- 根据正确答案的数量计算得分。
- 在变换后的图像上显示得分。
python
score = (correct / 5.0) * 100
print("[INFO] score:{:.2f}%".format(score))
cv2.putText(warped_new, "{:.2f}%".format(score), (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)
cv2.imshow("Original", image)
cv2.imshow("Exam", warped_new)
cv2.waitKey(0)
完整代码
python
import numpy as np
import cv2
def cv_show(name, img):
cv2.imshow(name, img)
cv2.waitKey(0)
def order_points(pts):
# 一共4个坐标点
rect = np.zeros((4, 2), dtype="float32") # 用来存储排序之后的坐标位置
# 按顺序找到对应坐标0123分别是 左上,右上,右下,左下
s = pts.sum(axis=1) # 对pts矩阵的每一行进行求和操作。(x+y)
rect[0] = pts[np.argmin(s)]
rect[2] = pts[np.argmax(s)]
diff = np.diff(pts, axis=1) # 对pts矩阵的每一行进行求差操作。(y-x)
rect[1] = pts[np.argmin(diff)]
rect[3] = pts[np.argmax(diff)]
return rect
# 将透视扭曲的矩形变换成一个规则的矩阵
def four_point_transform(image, pts):
# 获取输入坐标点
rect = order_points(pts) # 为上述排序的四个点
(tl, tr, br, bl) = rect # 分别返回给四个值,分别表示为左上、右上、右下、左下
# 计算输入的w和h值
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')
# 图像透视变换 cv2.getPerspectiveTransform(src,dst[,solveMethod])→ M获得转换之间的关系
# cv2.warpPerspective(src, Mp, dsizel, dstl, flagsl, borderModel, borderValue]]1])- dst
# #参数说明:
# src:变换前图像四边形顶点坐标/第2个是原图
# MP:透视变换矩阵,3行3列
# dsize:输出图像的大小,二元元组(width,heiqht)
M = cv2.getPerspectiveTransform(rect, dst) # 根据原始点和变换后的点计算透视变换矩阵M
warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight)) # 对原始图像,针推变换矩阵和输出图像大小进行透视变换,返回变换后的图片
# 返回变换后的结果
return warped
def sort_contours(cnts, method='left-to-right'):
reverse = False
i = 0
if method == "right-to-left" or method == 'bottom-to-top':
reverse = True
if method == 'top-to-bottom' or method == 'bottom-to-top':
i = 1
boundingBoxes = [cv2.boundingRect(c) for c in cnts]
(cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes),
key=lambda b: b[1][i],
reverse=reverse))
return cnts, boundingBoxes
ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1} # 定义一个字典,里面存放答题卡正确答案
image = cv2.imread(r'image/test_01.png')
contours_img = image.copy()
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
cv_show('blurred', blurred)
edged = cv2.Canny(blurred, 75, 200)
cv_show('edged', edged)
# 轮廓检测
cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[1]
cv2.drawContours(contours_img, cnts, -1, (0, 0, 255), 3)
cv_show('contours_img', contours_img)
docCnt = None
# 根据轮廓大小进行排序
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)
for c in cnts:
peri = cv2.arcLength(c, True)
approx = cv2.approxPolyDP(c, 0.002 * peri, True)
if len(approx) == 4:
docCnt = approx
break
# 透视变换
warped_t = four_point_transform(image, docCnt.reshape(4, 2))
warped_new = warped_t.copy()
cv_show('warped', warped_t)
warped = cv2.cvtColor(warped_t, cv2.COLOR_BGR2GRAY)
# 阈值处理
thresh = cv2.threshold(warped, 0, 255,
cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
cv_show('thresh', thresh)
thresh_Contours = thresh.copy()
# 找到每一个轮廓
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[1]
warped_Contours = cv2.drawContours(warped_t, cnts, -1, (0, 255, 0), 1)
cv_show('warped_Contours', warped_Contours)
questionCnts = []
# 遍历轮廓并计算比例大小
for c in cnts:
(x, y, w, h) = cv2.boundingRect(c)
ar = w / float(h)
if w >= 20 and h >= 20 and ar >= 0.9 and ar <= 1.1:
questionCnts.append(c)
questionCnts = sort_contours(questionCnts, method="top-to-bottom")[0]
correct = 0
for (q, i) in enumerate(np.arange(0, len(questionCnts), 5)):
cnts = sort_contours(questionCnts[i:i + 5])[0]
bubbled = None
# 遍历每一个结果
for (j, c) in enumerate(cnts):
# 使用mask来判断结果
mask = np.zeros(thresh.shape, dtype="uint8")
cv2.drawContours(mask, [c], -1, 255, -1)
cv_show('mask', mask)
thresh_mask_and = cv2.bitwise_and(thresh, thresh, mask=mask)
cv_show('thresh_mask_and', thresh_mask_and)
total = cv2.countNonZero(thresh_mask_and)
if bubbled is None or total > bubbled[0]:
bubbled = (total, j)
color = (0, 0, 255)
k = ANSWER_KEY[q]
if k == bubbled[1]:
color = (0, 255, 0)
correct += 1
cv2.drawContours(warped_new, [cnts[k]], -1, color, 3)
cv_show("warpeding", warped_new)
score = (correct / 5.0) * 100
print("[INFO] score:{:.2f}%".format(score))
cv2.putText(warped_new, "{:.2f}%".format(score), (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)
cv2.imshow("Original", image)
cv2.imshow("Exam", warped_new)
cv2.waitKey(0)
总结
此项目让我们见识了 OpenCV 在图像处理领域的强大功能,包括它对图像的一系列预处理、轮廓检测、透视变换等强大功能。Opencv库的功能和用法远不止于此,还需要我们更加深入的学习和探讨