文章目录
-
- 一、基于特征匹配的图像拼接技术
-
- [1.1 核心功能函数](#1.1 核心功能函数)
- [1.2 特征检测与描述](#1.2 特征检测与描述)
- [1.3 特征点匹配](#1.3 特征点匹配)
- [1.4 透视变换与图像融合](#1.4 透视变换与图像融合)
- 二、答题卡识别系统
-
- [2.1 坐标点排序与透视变换](#2.1 坐标点排序与透视变换)
- [2.2 图像预处理与轮廓检测](#2.2 图像预处理与轮廓检测)
- [2.3 答题区域识别与答案判断](#2.3 答题区域识别与答案判断)
一、基于特征匹配的图像拼接技术
图像拼接是将多张有重叠区域的图片合成一张全景图的过程,核心技术包括特征点检测、匹配和透视变换。
1.1 核心功能函数
python
def cv_show(name, img):
cv2.imshow(name, img)
cv2.waitKey(0)
功能分析:
- 这是一个图像显示辅助函数
cv2.imshow()创建指定名称的窗口显示图像cv2.waitKey(0)等待键盘输入,参数0表示无限等待- 按任意键后窗口关闭,程序继续执行
1.2 特征检测与描述
python
def detectAndDescribe(image):
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 将彩色图片转换成灰度图
sift = cv2.SIFT_create() # 建立SIFT生成器
# 检测SIFT特征点,并计算描述符,第二个参数为掩膜
(kps, des) = sift.detectAndCompute(gray, None)
# 将结果转换成NumPy数组
kps_float = np.float32([kpc.pt for kpc in kps])
# kpc 包含两个值,分别是关键点在图像中的 x 和 y 坐标。这些坐标通常是浮点数,可以精确地描述关键点在图像中的位置。
return (kps, kps_float, des) # 返回特征点集,及对应的描述特征
参数与方法解析:
-
cv2.cvtColor(image, cv2.COLOR_BGR2GRAY):颜色空间转换- 将BGR彩色图像转为灰度图像,减少计算复杂度
- 多数特征检测算法在灰度图上工作效果更好
-
cv2.SIFT_create():创建SIFT检测器- SIFT(尺度不变特征变换)对旋转、尺度缩放、亮度变化保持不变性
- 返回的检测器对象可用于关键点检测和描述符计算
-
sift.detectAndCompute(gray, None):检测关键点并计算描述符- 第一个参数:输入图像(灰度图)
- 第二个参数:掩膜,指定图像中哪些区域需要检测(None表示全图检测)
- 返回值:关键点列表和对应的描述符矩阵
1.3 特征点匹配
python
'''建立匹配器BFMatcher,在匹配大图训练集合时使用FlannBasedMatcher速度更快。'''
matcher = cv2.BFMatcher_create()
rawMatches = matcher.knnMatch(desB, desA, k=2)
good = []
matches = []
for m in rawMatches:
# 当最近距离跟次近距离的比值小于0.65时,保留此匹配对
if len(m) == 2 and m[0].distance < 0.65 * m[1].distance:
good.append(m)
# 存储两个点在featuresA、featuresB中的索引值
matches.append((m[0].queryIdx, m[0].trainIdx))
匹配策略分析:
-
cv2.BFMatcher_create():创建暴力匹配器- 计算查询图像(desB)中每个描述符与训练图像(desA)中所有描述符的距离
- 返回最匹配的k个结果(此处k=2)
-
比率测试(Ratio Test):
- 比较最佳匹配距离与次佳匹配距离的比值
- 比值小于0.65时认为是可靠匹配
- 有效过滤错误匹配,提高匹配质量
1.4 透视变换与图像融合
python
if len(matches) > 4: # 当筛选后的匹配对大于4时,计算视角变换矩阵。
# 获取匹配对的点坐标
ptsB = np.float32([kps_floatB[i] for (i, _) in matches]) # matches是通过阈值筛选之后的特征点对象
ptsA = np.float32([kps_floatA[i] for (_, i) in matches]) # kps_floatA是图片A中的全部特征点坐标
# 计算透视变换矩阵
# findHomography(srcPoints, dstPoints, method=None, ransacReprojThreshold=None, mask=None, maxIters=None, confidence=None)
# 计算视角变换矩阵,透视变换函数,与cv2.getPerspectiveTransform()的区别在与可多个数据点变换
# 参数srcPoints:图片A的匹配点坐标
# 参数dstPoints:图片B的匹配点坐标
# 参数method:计算变换矩阵的方法。
# 0 - 使用所有的点,最小二乘
# RANSAC - 基于随机样本一致性
# LMEDS - 最小中值
# RHO - 基于渐近样本一致性
# ransacReprojThreshold: 最大允许重投影错误阈值。该参数只有在method参数为RANSAC与RHO的时启用,默认为3
# 返回值:中值为变换矩阵,mask是掩模标志,指示哪些点对应内点,哪些是外点。
(H, mask) = cv2.findHomography(ptsB, ptsA, cv2.RANSAC, 10)
透视变换关键参数:
cv2.findHomography(srcPoints, dstPoints, method, ransacReprojThreshold)- srcPoints:源图像中的点坐标(图像B)
- dstPoints:目标图像中的点坐标(图像A)
- method=cv2.RANSAC :使用RANSAC算法估计单应性矩阵
- RANSAC对异常值(错误匹配)具有鲁棒性
- 随机选择最小样本集,找到使内点数量最多的模型
- ransacReprojThreshold=10 :重投影误差阈值
- 将源点投影到目标图像,计算实际点与投影点的距离
- 距离小于10的被认为是内点,否则是外点
python
# 应用透视变换
result = cv2.warpPerspective(imageB, H, (imageB.shape[1] + imageA.shape[1], imageB.shape[0]))
cv_show('resultB', result)
# 将图片A放入结果图像中
result[0:imageA.shape[0], 0:imageA.shape[1]] = imageA
cv_show('result', result)
cv2.imwrite('pingjie.jpg', result)


图像融合过程:
cv2.warpPerspective()将图像B变换到图像A的视角- 创建足够大的画布容纳两张图像
- 将原始图像A复制到结果图像的相应位置
- 由于透视变换,图像B会与图像A自然融合
二、答题卡识别系统
答题卡识别系统通过计算机视觉技术自动批改选择题,流程包括图像预处理、透视矫正、轮廓检测和答案判断。
2.1 坐标点排序与透视变换
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
坐标排序逻辑:
- 左上角:x+y值最小(最靠近原点)
- 右下角:x+y值最大(离原点最远)
- 右上角:y-x值最小(x相对较大,y相对较小)
- 左下角:y-x值最大(y相对较大,x相对较小)
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")
# 透视变换矩阵
M = cv2.getPerspectiveTransform(rect, dst)
warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
return warped
透视矫正步骤:
- 计算文档的实际宽度和高度
- 定义变换后的标准矩形坐标
- 使用
cv2.getPerspectiveTransform()计算变换矩阵 - 应用变换得到矫正后的图像
2.2 图像预处理与轮廓检测
python
# 预处理
image = cv2.imread(r'./images/test_01.png')
contours_img = image.copy()
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, ksize=(5, 5), sigmaX=0)
cv_show('blurred', blurred)
edges = cv2.Canny(blurred, threshold1=75, threshold2=200) # 修正参数名
cv_show('edges', edges)
预处理参数分析:
-
cv2.GaussianBlur(gray, ksize=(5, 5), sigmaX=0):高斯模糊- ksize=(5,5):高斯核大小,必须是正奇数
- sigmaX=0:X方向标准差,0表示根据核大小自动计算
- 目的:减少噪声,平滑图像
-
cv2.Canny(blurred, threshold1=75, threshold2=200):边缘检测- threshold1=75:低阈值,强度低于75的边被丢弃
- threshold2=200:高阈值,强度高于200的边被保留为强边缘
- 介于两者之间的边根据连通性判断
python
# 轮廓检测
_, cnts, _ = cv2.findContours(edges.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(contours_img, cnts, -1, (0, 0, 255), 3) # 修正:在原图上绘制轮廓
cv_show('contours_img', contours_img)
docCnt = None
轮廓检测参数:
cv2.findContours(edges.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)- RETR_EXTERNAL:只检测最外层轮廓
- CHAIN_APPROX_SIMPLE:压缩水平、垂直和对角线段,只保留端点
2.3 答题区域识别与答案判断
python
# 找到每一个圆轮廓
cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
warped_Contours = cv2.drawContours(warped_t.copy(), 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 0.9 <= ar <= 1.1:
questionCnts.append(c)
print(len(questionCnts))
气泡筛选标准:
- 宽度≥20且高度≥20:过滤小噪声点
- 宽高比0.9-1.1:筛选近似圆形的轮廓
- 确保只识别答题气泡,排除其他干扰
python
# 每排有5个选项
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) # -1表示填充
cv_show('mask', mask)
# 通过计算非零点数量来显示是否选择这个答案
# 利用掩膜(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) # 统计灰度值不为0的像素数
# 通过阈值判断,保存灰度值最大的序号
if bubbled is None or total > bubbled[0]:
bubbled = (total, j)
答案判断逻辑:
- 为每个气泡创建掩膜
- 使用
cv2.bitwise_and()提取气泡区域 - 统计区域内非零像素数量(涂黑部分)
- 选择非零像素最多的选项作为学生答案
python
# 对比正确答案
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)


可视化反馈:
- 红色轮廓:正确答案位置(如果学生答错)
- 绿色轮廓:正确答案位置(如果学生答对)
- 直观展示批改结果