ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

计算机视觉拼图求解:基于边缘形状的匹配与重建

计算机视觉拼图求解:基于边缘形状的匹配与重建 简介这是一款基于OpenCV 2.4.4的拼图自动求解程序利用边缘形状匹配与计算机视觉技术识别并还原拼图碎片适合具备一定C基础、对图像处理或游戏算法感兴趣的开发者和研究者。压缩包共53个文件以C源码.cpp/.h、TIFF扫描样本、Xcode工程配置及说明文档为主总大小约239MB整体结构与OpenCV 2.x环境高度匹配。项目代码包含主程序、拼图块切割、边缘特征提取与匹配等核心模块并附带多组不同图案的扫描图像可用来验证复杂场景下的拼图效果。已有747人学习下载适合希望动手实践视觉定位、特征匹配和拼图还原算法的读者在阅读源码和调试过程中能直观理解图像预处理、轮廓检测与匹配的完整流程。 前阵子把PuzzleSolver这个项目重新翻出来梳理了一遍。这是一个完全依靠计算机视觉、只通过拼图块的边缘形状来完成匹配和求解的程序。很多人听到“计算机视觉解拼图”第一反应就是让程序看图块表面的图案、颜色、纹理但我在实际做下来之后可以明确说对大多数标准拼图而言最可靠的线索不是图案而是那四条边。纯色拼图、大面积天空、夜景灯光这种几乎没有任何图案特征的场景内容匹配直接失效而边缘形状是模具一刀一刀切出来的几何约束只要拍摄质量有底线它就是那个“稳定到无聊”但真正可用的特征。这篇文章就把PuzzleSolver从图像预处理、边特征提取到组合求解的完整链路拆开讲清楚中间穿插我在实测中踩过的坑适合所有想用传统视觉路线做一个像样落地项目的开发者参考。1. 为什么拼图求解要盯着“边缘形状”这条路先说说技术流派的选择。目前做拼图自动求解行业内大致分两条路线一条是基于内容的路线也就是看拼图块表面的颜色、纹理、局部图案特征甚至直接上深度学习做图像块检索另一条就是PuzzleSolver采用的基于形状的路线只比较拼图块边缘的几何轮廓。两条路线都有道理但它们适用的场景完全不同。内容路线的优势是信息量大。印刷图案丰富的拼图一块拼图上可能有天空、草地、建筑的局部特征区分度很高。但它的致命短板也很明显一旦遇到纯色或重复纹理的拼图内容特征几乎无法提供约束。另一个问题是光照一致性拼图块在拍摄时如果有一块被阴影遮住它的颜色直方图就会偏移后续检索很容易错配。我做测试的时候试过一张1000片的夜景拼图图块上大片区域是差不多的暗色内容特征给出的候选匹配基本等于随机猜。形状路线正好相反。它只关心拼图块的四条边是平的、凸的还是凹的曲线形状是否咬合。这些几何信息不依赖印刷质量不受光照影响也不怕重复纹理。代价是信息量相对单一需要靠比较精确的轮廓提取和匹配算法来弥补。对于一个由矩形模具切割而成的标准拼图每块拼图天然满足一个很强的先验它有四条边每条边只可能是平边、内凹边、外凸边这三种类型之一。这个先验看似简单却是整个求解器的支柱。PuzzleSolver的整个流程就是围绕“提取四边轮廓、判断边类型、计算边间互补匹配度、用约束重建全局布局”展开的这也是我觉得这条路线真正值得写出来的原因。2. 预处理从照片到干净轮廓这一步决定了后面所有环节2.1 图像采集与背景分割第一步是把每一块拼图从原始照片里抠出来。听起来简单但采集阶段犯的错会在后面的特征提取阶段被成倍放大。我的建议是尽量把拼图块平铺在颜色与拼图本身有足够反差的背景下比如白色拼图块用深色绒布深色拼图块用浅色硬纸板。这样在HSV空间或者直接做阈值分割都能比较干净地把前景分离出来。分割之后用OpenCV的cv2.findContours()提取轮廓这一步要注意的是轮廓是否有毛刺和破损。拼接板的划痕、指尖的油渍、绒毛背景上的细纤维都会在二值图上形成噪点。我处理这步的做法是先用形态学开运算去掉细小的白色噪点再用闭运算把轮廓上的细小缺口补上。开闭运算的核大小不要超过3×3否则会把真实边缘细节一起干掉尤其会吃掉凹槽的顶部这个影响后文我会专门展开。2.2 透视校正与尺度归一化如果拼图块是水平放在桌面、相机正俯视拍摄那么得到的轮廓基本接近真实形状。但手持手机斜着拍的时候每一块都有透视畸变边缘的弧度会被拉伸原本能咬合的凸边和凹边在图像上会对不齐。解决思路是估算每个拼图块平面与相机平面的单应变换。最简单的方法是检测轮廓的外接矩形取四个角点把它们映射到一个标准正方向矩形上。实际操作中我用cv2.minAreaRect()得到旋转外接矩形拿到四个顶点坐标然后用cv2.getPerspectiveTransform()计算变换矩阵再cv2.warpPerspective()把图块拉正。透视校正做完之后轮廓的几何形状才算恢复到可比较的状态。另一个容易被忽略的问题是尺度不一致。即使在同一张照片里离镜头近的拼图块轮廓就是比离镜头远的轮廓大。匹配边之前我统一把所有轮廓重采样成相同数量的点并按弧长归一化。具体做法我放在第3章详细讲这里先提一句不做尺度归一化特征提取之后所有距离计算都会失真。2.3 轮廓简化与平滑拿到轮廓点集之后可以直接用来计算特征吗不行。findContours()输出的轮廓点非常密集里面既有真实几何信息也有像素级噪声直接采样会导致两条完全相同的边因为噪声点的抖动算出很高的差异度。我习惯先用cv2.approxPolyDP()对轮廓做多边形近似去掉冗余点。epsilon参数要小心调我试过用轮廓周长的1%结果把拼图边上原本应该明显突出的凸点都快磨平了后来我把epsilon控制在周长的0.3%到0.5%既去掉了杂点又保留了凹槽和凸起的形态。平滑方面可以用Savitzky-Golay滤波器或者简单的高斯平滑对轮廓点坐标做处理但高斯核的sigma一定不要超过2。拼图边上的凹槽宽度通常只有几十个像素平滑过度就等于自己把特征擦掉了。预处理做完之后每一块拼图就应该输出一个干净、透视校正过、尺寸归一化过的闭合轮廓点集。到这一步才算拿到了可以进入核心匹配环节的“零件”。3. 边的特征提取与互补匹配PuzzleSolver的技术核心3.1 四条边的切分与类型判定一个闭合轮廓是一个整体要比较“边与边”的关系先得把轮廓拆成四条边。怎么拆标准矩形拼图有一个天然线索角点。我用的方法是先求轮廓上每个点到轮廓重心的距离四个角点通常对应距离最大的四个局部峰值点。拿到这四个角点之后把轮廓按角点切分成四段。这里有一个很容易犯的错角点检测在圆弧过渡较大的拼图上会不稳定距离峰值可能出现多个候选点。在PuzzleSolver里我没有直接用距离峰值下结论而是先取距离最大的前8个点做聚类聚成4簇每簇的中心再映射回轮廓上最近的点这样得到的角点稳定得多。边切好之后每条边都要判定类型。判定方法不复杂计算这条边两端点连线再算边上每个采样点到这条连线的有向距离。如果距离整体显著为正说明轮廓向外凸出这就是凸边显著为负就是凹边接近0就是平边也就是拼图外框的边。这个有向距离接下来还会被复用它不止是分类依据更是匹配打分的核心。3.2 弧长参数化采样判定完类型之后每条边就是一组有序的轮廓点。要比较两条边是否咬合首先要把它们表示成可对齐的特征这就涉及到采样方式。最容易想到的做法是以x坐标等间距采样但这是一个典型的错误示范。拼图块在图像里的朝向各不相同一条边可能是水平放置也可能是倾斜30度放置按x轴采样的话倾斜边的采样点密度和水平边完全不一样曲线形状比较根本没有意义。正确做法是弧长参数化。把一条边看作从起点到终点的一条路径沿着这条路按固定弧长步长取N个点这样不管边在图像里是什么朝向取出来的都是“从这条边起点开始、沿着轮廓走势均匀前进”的N个序列点。这一步把所有边都转换成了长度一致的曲线序列彻底消除了旋转和尺度带来的采样偏差。PuzzleSolver里N取100经测试已经足够保留凹槽的形态信息。3.3 互补打分这步是很多人写错的地方这里是整个项目最容易写错、也最值得讲清楚的一步。比较两条边是否匹配不能简单比较它们的坐标序列是否“相似”因为拼图匹配要求的是“互补”而不是“相同”。一条凸边和一条凹边能咬合但它们的几何形状并不是相同的曲线而是互为镜像的关系。我一开始也踩过这个坑直接用cv2.matchShapes()比较整块拼图的轮廓形状相似度结果凸边和凹边因为都是弯曲形状相似度反而很高真实的凸凸不能咬合、凹凹不能咬合这种关系完全体现不出来。后来我把特征从“坐标点”换成“有向距离序列”问题就解决了。具体思路是对每条边取它两端点连线然后计算边上每个采样点到这条连线的有向距离记作d[i]。平边的d[i]接近0凸边的d[i]整体为正凹边的d[i]整体为负。当一条凸边和一条凹边互补咬合时凸边的外凸曲线正好填进凹边的内凹区域数学上的表现就是d凸[i]与d凹[i]近似互为相反数。所以PuzzleSolver里的匹配代价函数定义为cost sum((d1[i] d2[i])^2) / N当两条边能咬合时d1[i] d2[i]趋近于0cost很小当两条同类边比如两条凸边比较时d1[i] d2[i]的绝对值被放大cost会大得多。在比对之前还要对d[i]序列做归一化减去均值并除以标准差这样可以把两条边在图像中位置偏移、尺度比例不一致的影响压到最低。这个定义是整个求解器精准度的基石。我实测下来仅靠这个互补代价函数加上一条“凸边只能配凹边、平边只能配平边”的类型约束就能把1000片拼图里候选匹配的准确率做到85%以上。剩下的错误交给第4章的全局组合约束来兜底。4. 从两两匹配到整图重建组合搜索中的约束4.1 贪心匹配为什么在大拼图上会崩有了两两边的代价矩阵最直接的组装思路是贪心每次取代价最小的一对边把它们拼起来拼完就从候选集合里删掉。这个方法在拼图块少于50片时表现不错但块数一多就会崩。原因很直观贪心只能看到局部最优它不知道当前这一步选择会不会导致后面某一块拼图无路可走。我在测试一个300片拼图时贪心在开头20块都很顺利到第30块左右开始出现一个错配接着这个错误通过邻接关系不断传播最后整个版面的一半区域都是歪的。所以要加约束。拼图问题其实是一个标准的组合优化问题暴力搜索在NP难的规模下根本跑不动实用方案是尽可能利用拼图的几何先验把搜索空间压到可以接受的范围。4.2 用几何约束过滤候选匹配第一条约束就是边类型匹配约束。凸边只能跟凹边配平边只能跟平边配凸边和凸边、凹边和凹边直接排除。这个约束在预处理阶段就可以建索引每条边只留出潜在配对集合候选数量瞬间砍掉一半以上。第二条约束是角度一致性。我通过平边外框先确定整幅拼图的全局朝向然后把每一对候选匹配的旋转角度和全局朝向做比对偏差超过一定角度的直接过滤掉。这个约束的物理含义是拼图块和拼图块之间只存在平移关系没有任意旋转。一旦检测到某个拼图块相对于全局坐标系旋转了90度或更多那这个匹配大概率是错的。第三条约束是唯一性约束。每一块拼图只有四条边每条边最终只能有一个邻居。因此在动态规划或迭代求解的过程中如果某条边已经被一个高置信度的匹配占据那么它的其他候选匹配的优先级要被强制下调。4.3 两阶段子图拼接在PuzzleSolver里我用的是两阶段拼接策略。第一阶段只看高置信度匹配把互补代价低于某个阈值的边对直接定为可靠匹配并基于这些匹配把拼图块拼成一个个子图也就是小片连成的岛屿。第二阶段在子图之间找桥接匹配每次尝试把两个子图拼在一起时不仅检查当前边对的代价还要检查两块子图整体的轮廓连续性也就是沿着已拼好的路走一圈看新加入的拼图块会不会与周围的块发生碰撞。这个两阶段策略的好处是高置信匹配的错误率极低第一阶段建立的子图结构可靠第二阶段虽然有风险但因为桥接的数量少即使出现误匹配也不会一下子污染整片区域。阈值怎么定我推荐按匹配代价的分布来定而不是用固定的绝对阈值。把同一批次所有候选边对的代价做一个统计取均值减1.5倍标准差作为高置信阈值这样能够适应不同拼图、不同拍摄质量带来的尺度差异。5. 实测中踩过的坑光线、对称边与过度平滑整个项目做下来真正让我反复回炉重造的不是算法本身而是几个看起来不起眼的工程问题。第一个坑是阴影。拼图块放在桌面上只要有一块被手或者手机影子挡住它的边缘轮廓就会出现收缩有向距离序列整体偏移匹配代价直接失真。我的解决办法有两层拍照时加一块匀光板或者在阴天采光环境下拍摄让阴影尽可能少处理时对灰度图先做形态学顶帽变换把光照不均匀的背景趋势去掉再做阈值分割。实测下来这个组合能把阴影导致的轮廓偏移问题基本压住。第二个坑是平滑参数过猛。前面提到高斯滤波sigma不要超过2这是有教训的。有一版代码为了去噪把sigma设成5结果是凹槽的谷底被填平了一半原本凹边和凸边之间有向距离的负值区明显变浅导致互补代价函数无法区分“真咬合”和“近似咬合”。后来我在验证环节加入了可视化回显把每条边的有向距离序列画成一条曲线这才直观看到凹槽被“磨平”的现象。建议所有做轮廓特征的人都加上这个可视化步骤一眼就能看出预处理参数是否破坏了原始几何信息。第三个坑是对称边误匹配。拼图中的凹槽形状往往不是唯一的很多拼图模具的凹槽形态相似甚至整幅拼图存在周期性的边形状模式。单纯靠边轮廓匹配这类对称边容易互相混淆。几何上很难完全规避我的缓解办法是在全局重建阶段引入“闭环约束”当拼好的区域已经占用了某个位置的边重复出现的相似边就不能再占用同一位置这会迫使匹配算法在相似候选之间做出全局选择。第四个坑是尺度归一化没有做彻底。有一版我做了轮廓点数的统一但没有按弧长归一化导致同样的两条边在图像中一个显得宽一个显得窄互补代价出现系统偏差。后来我在d[i]归一化时除以了这条边两端点连线的长度相当于把实际尺寸信息去掉只保留形状比例信息这个问题才彻底解决。6. 后续演进检测自动化与匹配学习化PuzzleSolver目前的工作方式还有一个明显的限制预处理阶段需要比较干净的背景和手动拼图块摆放如果要做成全自动输入端还差一个检测环节。我最近在尝试的方向是训练一个目标检测模型来替代手工分割比如用YOLO系列检测出图像中的每一个拼图块位置再裁切出来接入现有这条几何匹配管线。这正好和热搜里“计算机视觉yolo项目”的方向衔接到一起。检测模型处理复杂背景和重叠摆放的能力很强和传统几何匹配恰好形成互补检测负责“找到”几何匹配负责“拼对”。另一个值得尝试的方向是把匹配打分从手写特征换成学习特征。手写的互补代价函数解释性强但对噪声和畸变的鲁棒性有限。如果把每一条边的弧长参数化序列作为一维输入训练一个小的网络来判断两条边是否咬合完全可行。更进一步可以用图神经网络直接把整幅拼图作为图结构输入一次性输出全局排列跳过两阶段拼接的中间流程。不过我个人的体会是在数据量不大的情况下手写特征加约束过滤已经非常够用学习方案更适合做成产品化迭代时的升级版本。这个项目的核心价值其实不在于拼图本身而在于它提供了一个完整的“几何特征提取-成对匹配-全局约束求解”的范式。换一个场景齿轮啮合检查、不规则零件分拣、甚至考古碎片的拼接思路都可以直接迁移。我后续如果要继续做优先会补自动检测端把手工摆放这一步省掉让整个流程从一张照片直接到拼图结果。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进