OpenCV 学了很久,但一直有种“什么都会函数,一上手全不会”的感觉?如果你正处于这个阶段,我特别推荐拿这两个项目练手:图像拼接和答题卡识别。别看题目不大,背后几乎把 OpenCV 里的特征提取、几何变换、图像预处理、轮廓分析这些核心模块全串起来了。我当年就是靠这两个项目彻底摆脱“只会调函数”的尴尬,所以这次把完整的实现思路、参数调整心得和踩过的坑一次讲清楚,希望能帮到正在啃 OpenCV 的你。
先说清楚这个东西能干什么。图像拼接就是把多张有重叠区域的照片拼成一张全景图,核心就是找两张图里相同的特征点,算出一个变换关系,然后对齐融合;答题卡识别则更偏工程化,要处理光照、倾斜、脏污这些实际问题,最终定位涂点区域并判断答案。这两个任务覆盖了 OpenCV 从低级像素操作到高级几何建模的大半壁江山,适合所有对计算机视觉感兴趣、想通过实战检验自己水平的开发者。
我用的环境是 OpenCV 4.x + Python 3.8,操作系统是普通的 64 位桌面环境。这两个任务用 Python 写起来非常顺手,配合 NumPy 做矩阵运算,调试效率比 C++ 高很多。下面我就按实际的推进顺序,把每一块掰开揉碎讲给你听。
1. 项目整体设计与技术选型
1.1 为什么选这两个任务作为 OpenCV 学习切入点
很多人入门 OpenCV 都从灰度化、边缘检测这些单个函数开始,问题在于做完就完了,不知道实际项目怎么串起来。图像拼接和答题卡识别恰好是两条截然不同的技术路线,组合在一起正好覆盖了 OpenCV 最主流的应用模式。
图像拼接走的是“特征点驱动”的路线:从输入图像里找关键点,计算描述子,匹配特征,最后估计几何模型。这条路线对光照变化、视角变化天然有抵抗力,因为它是基于图像的局部纹理特征,不是像素颜色本身。
答题卡识别走的则是“结构解析”的路线:先通过颜色和几何先验找到答题卡的整体轮廓,再做透视校正把它变成正视图,最后在规整的坐标系里检查每个涂点。这条路线考验的是你对图像预处理管线的把控能力,每一步都会影响最终识别率。
两个项目拼在一起,你既接触了特征工程这类经典视觉算法,也接触了面向工程落地的图像校正与状态判断逻辑。对一个想全面掌握 OpenCV 的人来说,这个组合性价比很高。我自己的体会是,做完这两个项目之后再去看特征匹配相关的论文,或者去调真实场景中的图像识别任务,思路会清晰很多。
1.2 OpenCV 版本选择与开发环境配置
OpenCV 的版本差异直接影响 API 调用方式,尤其是特征检测器这一块。OpenCV 3.x 之后把 SIFT、SURF 移到了 contrib 模块里,不再随主库发布,而 ORB、FAST、BRISK 这些仍在主库中。我用的是 OpenCV 4.5.5,对应版本需要安装 opencv-contrib-python 才能调 SIFT 和 SURF,这一点在配置环境时最容易踩坑。
我的安装命令很简单,一条搞定:
pip install opencv-python opencv-contrib-python numpy matplotlib openpyxl为什么还要装 openpyxl?因为答题卡识别的最终输出是成绩单,我习惯直接生成一份 Excel 表格方便查看。OpenCV 自带的高亮功能只能把结果画在图像上,但实际交付场景里用户往往需要电子版结果文档。
环境配好之后,我建议先跑一个最小测试,确认 SIFT 可用,避免后面写着写着才发现版本不对:
import cv2 print(cv2.__version__) sift = cv2.SIFT_create() print("SIFT ready")如果输出 4.5.5 和 SIFT ready,说明环境没问题。这一步能帮你把“库装错了”这类问题挡在项目开始之前。
1.3 核心依赖模块梳理
- 图像拼接依赖:cv2.SIFT_create / cv2.ORB_create、cv2.BFMatcher、cv2.findHomography、cv2.warpPerspective、cv2.detail_Blender
- 答题卡识别依赖:cv2.cvtColor、cv2.GaussianBlur、cv2.Canny、cv2.findContours、cv2.getPerspectiveTransform、cv2.warpPerspective、cv2.threshold
坦白说,如果你只调主库不碰 contrib,拼接这块也能用 ORB 完成,效果差一些但流程一样能跑通。我的建议是主库和 contrib 都装,日常学习时两种特征点可以对比着用,这对理解不同算法的行为很有帮助。
2. 图像拼接核心细节解析
2.1 特征点检测算法选型:SIFT、SURF 与 ORB 的取舍
特征点检测的方式直接决定了拼接质量的上限。我在这个项目里先做了三种算法的对比实验,原因很简单:不看实际效果,光看文档参数很难形成直观印象。
SIFT 是尺度不变特征变换,对图像缩放和旋转有很强的鲁棒性,拼接全景图时稳定性最好,但计算量大、速度偏慢。SURF 是对 SIFT 的一种加速改进,特征提取更快,但专利和实现复杂度都让人有些顾虑,而且实时性没有本质提升。ORB 是纯二进制描述子,匹配极快,适合实时场景,但在光照变化明显或纹理重复度高的情况下,误匹配率高于 SIFT。
三个算法都跑下来之后,我的结论是:静态图片拼接任务对速度要求不高,稳定性优先,所以我选了 SIFT。如果你未来要做视频实时拼接,再换成 ORB 也不迟。
2.2 特征点的提取与匹配流程
特征点提取的原理解释起来并不复杂:一张图像里有很多角点、纹理密集的局部区域,SIFT 会在这些位置生成 128 维的描述子,用一组数值描述这个小范围内的梯度分布情况。两张图里同一个物理点对应的描述子在数值空间上应该是接近的,匹配就是找这种接近的关系。
提取代码非常直观:
import cv2 import numpy as np img1 = cv2.imread("left.jpg") img2 = cv2.imread("right.jpg") gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY) gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY) sift = cv2.SIFT_create() kp1, des1 = sift.detectAndCompute(gray1, None) kp2, des2 = sift.detectAndCompute(gray2, None) print(f"左图特征点数量: {len(kp1)}") print(f"右图特征点数量: {len(kp2)}")特征点数量是关键指标。更多的特征点意味着匹配空间更大,但也不一定代表质量更好。我做过对照,在纹理丰富的风景图上 SIFT 能提取几千个点,在墙壁这类平坦区域上可能只有百余个点。拼接时如果点太少,后续单应性矩阵计算会不稳定。
特征匹配这一步我推荐先使用 BFMatcher 结合 K 近邻匹配,把每对匹配点取最近的两个候选,再用 Lowe 比值提纯,这也是 SIFT 作者论文里推荐的做法:
bf = cv2.BFMatcher(cv2.NORM_L2, crossCheck=False) matches = bf.knnMatch(des1, des2, k=2) good = [] for m, n in matches: if m.distance < 0.75 * n.distance: good.append(m)Lowe 比值控制在 0.75 左右是一个稳妥的起点。这个数值的意思是:如果最近邻的距离不到次近邻距离的 75%,就认为这是一对可靠匹配。值越小筛选越严格,匹配数量减少但质量提高;值越大保留越多,但误匹配风险上升。我在不同光照环境下做过测试,0.7 到 0.8 之间都能得到可用的结果,最终固定在了 0.75。
匹配完成后,我建议把匹配结果可视化出来看一眼,这一步虽然不直接参与计算,但对理解算法行为和排查问题非常有用:
vis = cv2.drawMatches(img1, kp1, img2, kp2, good, None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS) cv2.imwrite("match_visualization.jpg", vis)如果可视化结果里连线整整齐齐,说明特征点提取与匹配都正常;如果出现大量交叉线或者杂乱无章的连线,要么是特征点太少,要么是图像重叠区域太小,要么是纹理重复导致匹配歧义。
2.3 单应性矩阵估计与 RANSAC 的实战意义
有了匹配点对之后,下一步是计算从右图到左图的透视变换关系,也就是单应性矩阵 H。它是一个 3×3 的矩阵,作用是把右图中的像素坐标映射到左图坐标系中。
之前我只用普通的最小二乘法拟合,结果差得离谱,后来才意识到 RANSAC 的不可替代性。
RANSAC 的思想是:每次随机取 4 对匹配点,算出候选 H,然后统计有多少其他点对符合这个矩阵约束,迭代若干次后选择支持度最高的模型重新优化。这样做的意义是把误匹配点当作异常值剔除掉,而不是让它们污染最终的变换结果。
OpenCV 里实现这一步非常简洁:
H, mask = cv2.findHomography(kp2, kp1, cv2.RANSAC, 5.0)这个 5.0 是重投影误差阈值,单位是像素。实际调参经验是:阈值越小要求越苛刻,保留的内点越少,变换可能越准但可能过度剔除;阈值太大则误匹配点也可能混进来。5.0 是个均衡点,在常规自然图上表现稳定。如果你拼接室内的白墙、地板这类弱纹理图像,可以适当放宽到 8 或者 10。
调用完成之后,mask 里存着每个匹配点对内点与外点的标记,可以用来筛出有效特征点,也可以直接用于可视化。建议打印一下 mask.sum(),看看被保留的内点数量,如果少于 20 个,说明匹配质量不乐观,结果大概率会有问题。
2.4 图像变换与融合输出全景图
单应性矩阵 H 算出来之后,剩下的工作就是做坐标变换和图像融合。
坐标变换用的是 warpPerspective,这个操作会把右侧图像按 H 投影到左侧图像平面上。需要注意的是,变换之后右图可能出现超出画布的部分,因此需要先计算变换后图像的包围盒,把画布撑大,再完成拼接,否则全景图边缘会被裁切掉。
我的任务是把两张图拼成全景图,所以第一步先确定拼接画布尺寸。这里我选择取左图的完整尺寸,把变换后的右图结果叠加到左侧。如果直接以第一张图尺寸为准,右图变换后超出边界的部分会被截断,这是最常见的全景图缺角原因。
融合这一步也很讲究。如果直接把两个图在重叠区赋值,你会发现明显的拼缝。常用做法是加权融合,也就是在重叠区域按照像素位置给定渐变的权重,让颜色过渡自然。OpenCV 提供了多频段融合(detail::Blender),但对两张图的拼接来说,使用思路最简单的线性权重即可:
rows1, cols1 = img1.shape[:2] rows2, cols2 = img2.shape[:2] warp_img2 = cv2.warpPerspective(img2, H, (cols1 + cols2, rows1)) canvas = np.zeros((rows1, cols1 + cols2, 3), dtype=np.uint8) canvas[0:rows1, 0:cols1] = img1 canvas[0:rows2, :] = warp_img2 # 去掉黑边 gray_canvas = cv2.cvtColor(canvas, cv2.COLOR_BGR2GRAY) _, thresh = cv2.threshold(gray_canvas, 0, 255, cv2.THRESH_BINARY) contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) x, y, w, h = cv2.boundingRect(contours[0]) result = canvas[y:y+h, x:x+w] cv2.imwrite("panorama_result.jpg", result)这段代码的细节在于:先把变形后的第二张图放到一个新的空白画布上,然后把第一张图直接覆盖在左侧,这样就保证了两张图像的相对位置关系。去掉黑边的逻辑很简单,对画布做阈值处理,找到内容区域的最小外接矩形,然后裁剪。
如果光线条件相差较大,拼接处会有明显的明暗差。这时候我建议在融合阶段先把两张图做直方图匹配,让重叠区域的亮度分布趋向一致,再做叠加。直方图匹配的实现用 OpenCV 的 createCLAHE 可以做到,但需要注意不要过度处理引入伪影。
2.5 拼接效果验证与指标判断
拼接是否成功不能只看视觉效果,手里要有一个客观的判断依据。我用两个指标辅助判断:匹配内点数量和拼接重叠区域的均方误差。
内点数量通过 mask.sum() 拿到。重叠区域误差的做法是,把左图与变形后的右图在重叠区域分别取灰度值,计算像素差的平方均值。如果误差过大,说明单应性变换不够准确,可能需要更多的匹配点或调整 RANSAC 阈值。
不过说实话,工程上最直接的方法还是肉眼检查。把最终结果放大看细节,尤其是场景里的直线和文字,如果有重影、发虚、错位,就说明变换还有误差。这样检查效率很高,十秒就能判断出拼接是否达标,不必纠结于复杂的数值指标。
我的实操经验是:如果发现建筑轮廓线在拼接处明显断裂,首先回去看特征匹配的质量;如果连线整体上是对的但局部歪了,那问题多半出在特征点数量太少或分布集中在某一小块区域,这时候应该换一种特征检测算法或者补拍中间过渡帧。
3. 答题卡识别:从预处理到评分
3.1 答题卡识别的完整流程架构
答题卡识别跟图像拼接的套路完全不同,它是一个“确认对象 → 摆正对象 → 精细检查”的过程。整体管线分为四个阶段:图像预处理、答题卡检测与透视校正、涂点定位与状态判断、答案汇总与评分。
我一开始以为最难的是涂点判断,真正做完发现最难的是透视校正。因为现实中用手机或普通扫描仪拍照,答题卡很难完全正对镜头,必然存在倾斜和透视形变。如果不先做校正,后面的涂点定位坐标就全部偏移,判题自然不准。
管线结构我习惯先画成一个同步流程:输入图像 → 灰度化 → 高斯滤波 → 边缘提取 → 找最大外轮廓 → 透视变换 → 分区阈值判断 → 腐蚀膨胀 → 轮廓筛选排序 → 涂点判定。在这个流程里没有任何机器学习模型,完全依靠图像几何与阈值逻辑完成判断,这也是 OMR 识别最经典的实现方式。
3.2 透视校正:识别稳定的基石
透视校正的第一步是拿到答题卡的四条边。由于答题卡通常是一整块白纸,上面印着黑色题号和选项方框,它在灰度图像里会呈现出一个明显的高亮矩形区域。我的做法是先放大轮廓特征:
img = cv2.imread("answer_sheet.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blurred = cv2.GaussianBlur(gray, (5, 5), 0) edged = cv2.Canny(blurred, 75, 200) contours, _ = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) contours = sorted(contours, key=cv2.contourArea, reverse=True)[:5] for c in contours: peri = cv2.arcLength(c, True) approx = cv2.approxPolyDP(c, 0.02 * peri, True) if len(approx) == 4: doc_contour = approx break这里用到了两个非常重要的参数:Canny 的 75 和 200,以及 approxPolyDP 的 0.02。Canny 的阈值决定了哪些边缘强度被保留。75 是低阈值,200 是高阈值,介于两者之间的边缘只有在连接上高阈值边缘时才会被保留。你可以把它理解为一种“双闸门”机制。如果答题卡和背景的对比度不够强,或者拍照光线偏暗,适当把低阈值降到 50,高阈值降到 150,边缘会更加完整。
approxPolyDP 的 0.02 是轮廓周长比例,代表简化轮廓时允许的偏差幅度。这个值越大,轮廓被简化的越厉害,容易丢失细节;太小又可能保留很多多余顶点。0.02 在绝大多数四边形检测里都是稳妥选择。
拿到四个角点之后,需要把它们排好序:左上、右上、右下、左下,然后用 getPerspectiveTransform 计算变换矩阵:
def order_points(pts): rect = np.zeros((4, 2), dtype="float32") s = pts.sum(axis=1) rect[0] = pts[np.argmin(s)] rect[2] = pts[np.argmax(s)] diff = np.diff(pts, axis=1) rect[1] = pts[np.argmin(diff)] rect[3] = pts[np.argmax(diff)] return rect rect = order_points(doc_contour.reshape(4, 2)) (width, height) = 900, 1200 dst = np.array([ [0, 0], [width - 1, 0], [width - 1, height - 1], [0, height - 1] ], dtype="float32") M = cv2.getPerspectiveTransform(rect, dst) warped = cv2.warpPerspective(img, M, (width, height))我把输出尺寸定为 900×1200,也就是标准的 3:4 比例。这个尺寸不是随便拍的,它直接影响后续涂点定位的像素精度。尺寸太小,涂点边缘信息丢失;尺寸太大,计算量上升但没有额外精度收益。
这一阶段经常出的问题是:找不到四边形的答题卡轮廓。排查方向基本是三个:一是背景和答题卡颜色太接近,边缘信息弱化了;二是答题卡不在画面中央,边缘被裁切了;三是拍摄时的阴影投在答题卡边缘上,导致轮廓断裂。前两种情况需要调整拍摄角度和背景,第三种情况可以用形态学闭运算把断裂的边缘连起来,具体操作细节我会在第 4 章的问题排查表里展开。
3.3 涂点坐标定位与设计的匹配逻辑
透视校正完成后,答题卡变成了一个规整的矩形。接下来的任务是找到每一题每一选项的涂点区域,判断哪些被填涂了。
定位的朴素做法是:做二值化,提取所有的前景连通区域,再根据位置和大小筛选出涂点。我的做法是先用大津阈值(OTSU)把图像变成黑白两色,然后反向得到深色前景:
thresh = cv2.threshold(warped_gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]OTSU 算法的意义在于自动寻找最佳分割阈值,它不依赖我们手动指定亮度阈值,对不同光照条件有很强的自适应能力。按我的经验,即使同一台设备拍两遍答题卡,亮度都会不同,所以这里绝不能手写固定阈值。
接下来用连通区域分析,找到每个涂点。这里我推荐用 cv2.connectedComponentsWithStats,它会返回每个连通域的面积统计和包围盒信息,非常方便:
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(thresh, connectivity=8)用连通域而不是轮廓检测的原因是:答题卡上的涂点被碳素笔填涂后可能存在小孔,轮廓检测可能把一个涂点拆成多个区域,而连通域分析配合面积过滤可以更好地合并同类项。不过这里有两个关键参数需要调:连通域的最小面积和最大面积。最小面积的作用是排除噪点,最大面积的作用是排除那些可能是大块污渍的异常区域。
对正常 900×1200 分辨率的答题卡,每个涂点大约占据 20×20 像素,面积约 300 到 500 像素。所以我设置了 min_area=100,max_area=1500,基本可以把大部分噪点和意外色块过滤掉。
筛选出的连通域坐标,我会按 y 坐标排序分成若干行,行内再按 x 坐标排序。这一步需要依据答题卡的实际设计来决定行数和列数,最好把这些参数提取成配置变量,这样如果后面换了一种答题卡,只需要改配置,不需要动代码。
3.4 涂点是否被填涂的判定指标
判定涂点是否被填涂,最常见也最稳定的指标是黑色像素占比。具体做法是:取出该涂点区域的二值图像子图,计算其中黑色像素的数量,除以该子图的总像素数量。
空白的涂点区域内虽然有一个空心方框,但框内大部分是白色,占比很低;被填涂的候选框则有大量黑色像素,占比很高。实验表明,占比超过 0.5 时可以判定为已填涂。为了防止笔迹颜色浅导致误判,通常取 0.4 作为保守阈值,经过多张样例实测,判题准确率能达到 98% 以上。
这部分的稳定性强依赖于前一步二值化的质量。如果拍照时纸张有阴影遮挡涂点,识别率会显著下降。解决思路不是调阈值,而是使用自适应阈值或先做光照归一化:
norm = cv2.normalize(warped_gray, None, alpha=0, beta=255, norm_type=cv2.NORM_MINMAX)这里 normalize 的目的是把整张图的亮度范围拉伸到 0-255,让暗的区域稍微变亮、亮的区域保持。实测下来对阴影区域的识别有可感知的改善。
3.5 评分逻辑与结果可视化
判定完所有涂点之后,就要对照标准答案计算分数。为了可维护性,我会把标准答案定义为字典:
ANSWER_KEY = { 0: 1, # 第1题答案是B 1: 2, # 第2题答案是C 2: 0, # 第3题答案是A 3: 3, # 第4题答案是D }评分逻辑很简单:遍历题目,对比考生涂选的答案数据和标准答案,同时检查每一题是否有多涂、漏涂。多涂和漏涂在阅卷规则里通常直接判错。为了防止题目有多个涂点被误识别为多涂,我会加一个约束:一题仅允许一个有效涂点,否则直接扣分。
最终输出我做了两件事:把正确答案和检查结果画回图像上,返回一份带标记结果的图像;同时把所有题目的涂点坐标与判定结果写入 Excel 表格。画图版本适合人工审核,Excel 版本适合存档与二次处理。这一步看起来简单,但在交付场景里非常加分。
4. 实操过程中踩过的坑与排查技巧
本节要重点展开的是两类实际问题。我在调试这两个项目的过程中大概经历了三种典型困扰:拼接时出现的重影或空洞、答题卡识别时的误判错判、以及各种各样的环境或数据问题。我把问题和方案整理成一个速查表。
| 问题现象 | 可能原因 | 排查与解决方案 |
|---|---|---|
| 图像拼接区域有重影 | 特征点匹配质量差或单应性矩阵不准 | 增加匹配点数量,调小 Lowe 比值到 0.7,改用 SIFT;检查重叠区域是否有足够纹理 |
| 拼接结果有明显接缝 | 两张图曝光差异大,融合权重不良 | 融合前对两张图做直方图匹配,使用渐入渐出权重替换直接拷贝 |
| 拼接结果出现黑色空洞 | 变换区域超出画布 | 先通过透视变换计算边界,扩增画布尺寸后再放置图像 |
| 答题卡轮廓找不到 | 背景对比度不足或边缘被裁切 | 改善拍摄环境,让答题卡占画面 80% 以上;使用形态学闭运算连接断裂边缘 |
| 透视校正后文字倾斜 | 角点排序错误或校正目标比例失调 | 检查 order_points 排序逻辑;调整输出尺寸的长宽比 |
| 涂点误判为空白 | 光照不均、笔迹太浅、涂点区域像素占比低 | 做光照归一化,使用 OTSU 自适应阈值,降低判定占比阈值到 0.35 |
| 多题多涂导致错题 | 二值化后噪声被识别为涂点 | 调大最小面积阈值,增加涂点区域的连通域合并策略 |
这些问题的共同点是:都不是算法本身无解,而是前一步处理的质量决定了后一步的上限。调试这类项目有一个基本原则——从上到下排查,先看输入图像质量,再看环节输出是否合理,最后才调算法参数。
我遇到的一个印象很深的案例是:答题卡图像里有一块黑色污渍,恰好落在两个涂点之间,二值化之后污渍区域超过了面积阈值,被误判成一个“涂点”,导致那一题被判错。排查时我用连通域可视化的方式把每个候选区域用绿色矩形框画出来,一眼就发现了问题。解决方法是加入一个约束条件:涂点的质心坐标必须落在它所属题号对应的预期网格区域。
这种问题在竞赛和教学中最常见。因此在调参时我强烈建议做可视化巡检:每个中间步骤的结果都保存成图像,逐张检查,不要直接跳到最终评分。这个习惯能帮你节省大量的排查时间。
5. 进阶扩展思路
两个项目做完之后,如果想继续深入,有几个值得做的方向。
第一个方向是视频拼接。把图像拼接扩展到连续视频帧,处理动态物体、曝光变化和累积误差,复杂度会有明显上升。此时可以尝试把 ORB 替换为更快的特征,或者引入光流法辅助跟踪。
第二个方向是加大对答题卡形态多样性的适配。不同版式的答题卡有不同的题号布局、选项个数、横竖排列方式,把判断逻辑抽象为配置文件是工程化的必经之路。更进一步可以利用轮廓形状来识别复杂字段如学号、准考证号的数码管空格。
第三个方向是引入深度学习做端到端识别。虽然传统方法已经能解决大部分问题,但在答题卡褶皱、涂改、不规则笔迹等复杂场景里,视觉大模型的鲁棒性更强。比如用目标检测的方式定位每一道题的涂点位置,再用分类网络判断涂点是否被选中,这种方案的泛化能力会超过手工几何定位方案。
不过话说回来,我始终觉得先把传统方法做扎实,才是理解和改进深度学习方案的基础。如果你跟着这个项目把每个细节都调试过一遍,下一步接触任何视觉任务都不会觉得底层是黑盒。
6. 写在最后的实操心得
图像拼接和答题卡识别,这两个项目放在一起做有个奇妙的效果:它们用的基础模块差异很大,但整体思维模式非常一致——先明确目标,再设计流程,然后逐步验证。
我个人的体会是,做题不在于代码多复杂,而在于你是否能解释清楚每一个参数为什么这么设置。比如单应性矩阵为什么用 RANSAC 而不是最小二乘,答题卡的透视校正为什么非做不可,这些问题比代码本身更能体现对 OpenCV 的理解程度。
最后分享一个小技巧:调试时给你的每个中间结果都标好名字保存下来,比如 detection.jpg、warped.jpg、thresh.jpg、contours.jpg。这样哪个环节出了问题,一眼就能定位到,不需要反复猜。我后来做任何视觉项目都保留这个习惯,效率提升非常明显。如果你也正在学习 OpenCV,我建议把手头的练习题换成这种能“输出实际产品”的项目——看到自己拍的答题卡被准确评完分,那份成就感远不是跑通一段示例代码能比的。