数独照片识别与求解:OpenCV透视校正到答案回填实战

发布时间:2026/10/20 21:25:36
数独照片识别与求解:OpenCV透视校正到答案回填实战 几个月前接到个小需求拿手机拍一张数独照片几秒内把答案算出来并叠回原图上。我当时觉得这事不难——数独识别不就是 OCR 加回溯求解嘛现成库一调就完事。真正动手才发现随手拍的照片里网格是歪的、数字是糊的、反光会把白格变成灰格光线一变阈值就崩。这个看似简单的 demo实际上把计算机视觉里最经典的一整套流程全走了一遍图像预处理、边缘检测、几何变换、字符识别、坐标映射。今天就把我当时怎么一步步从“照片”变成“答案图”的过程完整拆开讲想入坑计算机视觉的朋友可以照着这条路走一遍比单纯看理论印象深刻得多。1. 为什么偏偏是数独一个任务串起计算机视觉的基本功1.1 数独图像处理要面对的真实问题很多入门教程喜欢拿“识别一张卡牌”“检测一个红绿灯”当例子但那些任务都太单薄了。数独这个场景最妙的地方在于一张真实拍摄的数独照片里几乎包含了所有经典干扰透视畸变手机不可能完全正对着纸面拍网格必然是个不规则的四边形切出来是梯形的。光照不均室内灯光、窗外阳光、手指阴影都会让同一张纸上的亮度差一大截。线条断裂铅笔画的格子线、打印墨迹偏淡、皱褶处的反光会让边框或内部格线断掉。前景混乱网格旁边可能有别的文字、手指、桌面纹理全都需要排除。数字风格多样印刷体、手写体、粗细不一甚至还有写一半的。这些问题在教科书里是每个独立章节在数独项目里是一连串必须连贯处理的现实障碍。做完这个项目等于把图像去噪、二值化、边缘检测、轮廓查找、透视校正、图像分割、字符识别、坐标变换、结果可视化全打通了一遍。拿我自己举例最初我直接对原图做全局二值化结果阴影区域里的数字全被当成背景吃掉了后来才换成自适应阈值最初我假定网格是正的直接均分 9 份结果歪了的照片切出来的格子边缘全是斜线数字被切得七零八落。这些坑每一个都对应一个经典知识点踩一遍比看十遍书都管用。1.2 从像素到答案的完整链路整个项目我拆分成了 9 个步骤每个步骤都有明确的输入输出步骤操作输入输出对应知识点1图像加载与缩放手机照片统一尺寸的彩色图图像格式、缩放插值2灰度化与去噪彩色图干净的灰度图色彩空间转换、高斯滤波3二值化/边缘检测灰度图前景与背景分离的二值图阈值分割、边缘检测4提取网格外轮廓二值图数独外边框的四个角点轮廓查找、多边形逼近5透视校正原图角点正视角的方形数独图单应矩阵、透视变换6切分 9x9校正后的图81 个格子的小图投影法、图像切片7数字识别格子小图81 个数字或空模板匹配/CNN8数独求解81 个数字完整答案矩阵回溯搜索9结果叠回原图答案变换矩阵带答案的标注图逆透视、坐标映射很多人一开始会想能不能搞一个端到端深度学习模型输入照片直接输出答案技术上可行但作为学习项目非常不推荐。端到端模型是个黑盒哪一步错了你根本不知道是“网格没找到”还是“数字没认出”也没法逐步调试。分步管线的好处是每一步都能输出中间结果图出了问题看一眼就知道坏在哪一步。这个调试思路在真实工程里比任何花哨算法都重要。1.3 工具选型Python 加 OpenCV 就够了整个项目我用的是 Python 和 OpenCV求解部分自己写了个回溯大约 50 行识别先用模板匹配跑通最后换成了简单卷积网络。为什么不直接用 C因为前期大量工作是调参数、看中间结果Python 的交互式环境效率高得多。等以后要做实时视频流或者部署到手机上再换成 C 或者模型端侧推理那是后话。OpenCV 里我主要用到的模块就几个cv2.findContours找轮廓、cv2.approxPolyDP做多边形逼近、cv2.getPerspectiveTransform算透视矩阵、cv2.warpPerspective做校正变换、cv2.matchTemplate做模板匹配。每个函数单独拿出来都不难难的是把它们串起来的时候才知道参数怎么配合。2. 拍照时网格是歪的先做预处理和透视校正而不是急着识别2.1 灰度化、去噪和自适应阈值的选择逻辑拿到一张手机照片第一件事绝对不是识别数字而是先把图像变干净、把网格找出来。我先用cv2.cvtColor转灰度然后用 5x5 的高斯滤波去噪。高斯滤波的核大小是个经验值太小去不掉传感器噪声太大容易把数字边缘糊掉。关键的一步是二值化。我最初用了全局阈值cv2.threshold发现只要照片上有不均匀阴影全局阈值就废了——阴影区的白底被当成黑数字直接消失。后来换成了cv2.adaptiveThreshold它的原理是对每个像素和它周围一个小邻域的平均亮度做比较相当于每个局部区域自动选取阈值。这就是为什么它能在光照不均的情况下仍然把数字和背景分开。我用的参数是blockSize15, C2意思是每个像素和周围 15x15 窗口的平均灰度比较谁更暗超过 2 个灰度级就置为黑。如果是在强光、带纹理的纸面上拍单纯自适应阈值偶尔会把纸面纹路当成前景。这种情况我建议先试Canny边缘检测再做轮廓查找效果往往更稳。2.2 用轮廓查找定位数独外边框二值化之后数独网格的外边框会形成一个大的闭合轮廓。我使用cv2.findContours提取所有轮廓然后筛选先按面积过滤只保留占整张图面积一定比例以上的轮廓这一步能去掉桌面纹理、手指、纸屑。再用cv2.approxPolyDP对方形轮廓做多边形逼近筛选出四边形。approxPolyDP的 epsilon 参数我取周长的 2%这个值很重要太大导致四边形失真太小又把弯曲的边界拆成很多段。数独外框在图像中通常是面积最大的四边形所以按面积排序取最大那一个。这里有个很容易翻车的点如果照片里数独边框恰好和照片边缘重合了一部分那么最大的“轮廓”可能把整张照片都包进去了不收敛成一个四边形。我后来加了一个校验四个点形成四边形之后检查四条边的长度是否接近四个角是否接近直角。如果校验不通过就把这个候选丢弃换下一个轮廓。2.3 透视变换不是简单的裁剪是平面单应找到四个角点之后它们对应的是数独网格在真实纸面上的一个正方形。我们要把照片里那个任意四边形“拉正”成正方形这个操作叫透视变换数学上用一个 3x3 的单应矩阵描述OpenCV 里cv2.getPerspectiveTransform会根据四对对应点直接算出这个矩阵。我用下面的代码完成校正import cv2 import numpy as np def order_points(pts): # 四个点按左上、右上、右下、左下排序 rect np.zeros((4, 2), dtypefloat32) s pts.sum(axis1) rect[0] pts[np.argmin(s)] rect[2] pts[np.argmax(s)] diff np.diff(pts, axis1) rect[1] pts[np.argmin(diff)] rect[3] pts[np.argmax(diff)] return rect src order_points(approx.reshape(4, 2)) dst np.array([[0, 0], [450, 0], [450, 450], [0, 450]], dtypefloat32) M cv2.getPerspectiveTransform(src, dst) warped cv2.warpPerspective(img, M, (450, 450))这段代码里最有信息量的是M。这个矩阵不只在这次校正中用到后面把答案写回原图时需要用它的逆矩阵做反向映射。我一开始没保存M第五步往回画的时候还得重新算后来学乖了一开头就把M和M_inv cv2.linalg.inv(M)都存好。2.4 三个经典翻车场景边框断裂、反光和阴影边框断裂铅笔线断了一截findContours找出来的轮廓是断开的无法形成闭合四边形。解决办法是在二值化之后先做一次闭运算用小核把断点连起来。核用 3x3 就够了核太大会把数字和附近的格线黏在一起。反光照片上有强反光时反光区域会变成纯白整个网格看起来缺了一个角。这种情况自适应阈值的局部窗口也会失效。我实测下来先用cv2.medianBlur或者用Canny检测边缘再补一次膨胀比单纯调阈值管用。阴影边缘纸面上有手指阴影且阴影恰好压在边框线上。这时外轮廓面积可能变小。我给轮廓筛选加了一个“最小包围边长”的约束同时把输入图像先等比缩放到固定宽度保证不同距离拍摄的轮廓面积在一个可比尺度下。3. 切开 9x9网格分割里的坐标陷阱3.1 为什么不能直接均分九等份很多人透视校正完就认为大功告成直接把 450x450 的图像切 9 块每块 50x50。我一开始也这么干结果发现网格的粗线本身占了 2~4 个像素直接按均分会导致某些格子切到了相邻数字某些格子把边框线算进去了。更麻烦的是校正之后如果仍有 1 度左右的旋转残余越靠近右下角的格子误差越大。所以必须找到真正的格线位置而不是假定它们均匀分布。3.2 用投影法找到格线的精确位置格线提取我用的方法是投影法。对校正过的二值图分别按行和列做像素值求和白线格线所在的行或列会有明显的高峰非线区域数值很低。找到这些峰值的位置就得到了所有横线和竖线的坐标。def find_lines(binary_image, axis0): proj binary_image.sum(axisaxis) lines [] in_line False for i, v in enumerate(proj): if v threshold and not in_line: start i in_line True elif v threshold and in_line: end i mid (start end) // 2 lines.append(mid) in_line False return lines注意我取的是每一条连续高亮区间的中点而不是峰值点。因为格线可能有 2~3 个像素宽取中点可以消除宽度带来的偏差。因为数独内有 10 条横线和 10 条竖线检测到 10 个横坐标和 10 个纵坐标就得到了 10x10 个交叉点。用这些交叉点的坐标作为切割边界每个格子两侧各留 2 个像素的余量再把格子小块裁剪出来。3.3 切割后的格子校验与空白格判别切出来的格子小图我在送入识别前做三件事尺寸归一化把所有 48x48 的格子小图直接 resize 到统一尺寸识别网络的输入固定。背景清理检查格子小图边缘是否有非零像素如果有说明格线残留做一次腐蚀边缘处理。空白判定统计小图中黑色像素占比如果低于某个阈值比如 2%判定为空格不送识别器直接标记为 0。这个步骤能大幅减少误识别因为空格如果硬送进分类器反而可能被莫名其妙分类成某个数字。3.4 坐标陷阱直线有斜率交叉点需要矩心校正用 HoughLines 提取格线时若直接求两线交点由于照片歪斜矫正后仍有残余旋转横线和竖线并不绝对垂直直接相交得到的点可能偏离真实网格节点。我的做法是把检测到的所有交叉点收集起来然后对每个真实节点周围的候选点求平均值相当于做一次聚类的矩心。这一步对后面的答案回填非常重要坐标偏几个像素原图上数字位置看起来就歪了。有一个小经验校正后的图像尺寸我固定用 450x450一方面保证格子约 50x50 大小适合识别另一方面让交叉点坐标基本是整数调试时肉眼看一下输出数组就能发现问题。4. 数字识别是计算机视觉和机器学习的交接处4.1 模板匹配不需要训练数据也能跑的 MVP先把识别这条线跑通我用的是模板匹配。做法是准备 10 张标准打印数字模板0~9我直接用绘图软件生成每张缩放到 32x32。然后对每个格子小图做同样的预处理再用cv2.matchTemplate加TM_CCOEFF_NORMED计算相似度取分数最高的作为结果。模板匹配的优点是零训练成本、代码量小打印体数字识别率很高。我实际测试了几张印刷清晰的数独照片正确率大概在 90% 左右。缺点是极其死板换一种字体、换一个笔迹分数立刻掉下去数字稍微旋转一点也会失效。刚开始调试 pipeline 时用它当占位符是非常明智的因为你需要先确认网格分割、透视校正这些环节是对的再去优化识别。4.2 简单卷积网络更稳但要会做数据增强流程稳定之后我把识别模块换成了一个小型卷积网络三层卷积加池化接两个全连接层输入 48x48 灰度图输出 10 类。训练数据用的是公开手写数字数据集训练集几万张足够用了。但直接拿公开数据集训练出来的模型放到数独照片上表现并不好。原因很简单公开数据集里的数字是居中的、手写的、笔画集中的而数独照片里的数字可能偏低、偏大、边缘残缺。我的解决办法是训练时做数据增强随机平移最多 3 个像素、随机缩放 0.9~1.1 倍、随机旋转正负 5 度。这么一弄真实照片上的识别率明显提升误判主要集中在“3”和“8”、“1”和“7”这几组容易混淆的数字上。4.3 识别前的归一化细节保持宽高比而不是直接拉伸把格子小图送进网络之前我踩过一个细节坑如果直接cv2.resize拉伸到 48x48会把原本细长的“1”拉宽成粗胖的“1”也会把横向较宽的“3”压扁导致误识别。正确的做法是先找数字的连通域必要时用cv2.connectedComponentsWithStats找到数字包围盒然后把包围盒按比例缩放到 40x40 左右再居中放进 48x48 的白底画布。这样一来数字的原始宽高比保留住了网络输入也不会因为白边太多而丢失特征。4.4 误识别的常见来源和投票策略总结下来误识别基本来自四类情况空格被识别成数字所以空白阈值必须先判断数字偏小、笔画太细导致特征不足格线残边进入小图干扰了分类“6”和“8”、“9”和“3”这类结构相似的数字在分辨率低的情况下确实容易混我最后的方案是引入投票对同一张格子图分别用原始图、轻微膨胀图、轻微腐蚀图各识别一次三个结果取多数。如果三个结果完全不一致就标记为低置信度人工或按概率最高的结果处理。这个投票策略简单有效能把整体识别率达到 95% 以上。5. 把答案写回原图坐标逆变换的最后一公里5.1 一开始就要保存单应矩阵别返回去找前面的步骤做完识别出的 81 个数字空位为 0组成了一个棋盘矩阵。求解器我用的是回溯纯 Python 跑一个数独题大概 30~50 毫秒完全够用。问题是答案最后要显示在用户拍的那张歪斜照片上这时候如果当初没保存透视矩阵就得重新找一遍角点再算一次非常麻烦。所以再次强调在第 2 步计算M cv2.getPerspectiveTransform(src, dst)时立刻保存M和M_inv。把答案从校正图坐标映射回原图坐标用M_inv对校正图上的点做透视变换即可。pts_corrected np.array([[col * cell, row * cell] for col in range(9) for row in range(9)]) pts_original cv2.perspectiveTransform(pts_corrected.reshape(-1, 1, 2).astype(np.float32), M_inv)5.2 反向映射到原图写数字的细节得到原图坐标后我在每个格子内部用cv2.putText写答案。这里有一个显示上的问题直接把答案覆盖到题目数字上会遮住原有题目用户看不出来哪个是题目哪个是答案。我的做法是题目原有的数字保留黑色答案数字用深红色写在格子中心偏下的位置避免和原有数字重叠如果格子里原本有数字把答案写到右下角空白处写字前最好先根据每个格子的实际四边形边长估算字体大小。校正图上格子是 50x50但原图上距离不同、透视不同远处格子看起来小近处格子看起来大。直接用一个固定字体大小会让远处数字挤满格子、近处数字显得太小。我按照每个格子四边形的平均边长比例动态计算字体大小效果会自然很多。5.3 防止答案遮挡和视觉混乱的处理还有一个不起眼但很重要的点当答案数字和题目数字在同一个格子里时要处理遮挡。我测试时发现直接把红色答案写在题目黑色数字正上方不仅看不清还容易被用户误认为印刷重叠。后来我把答案统一放在格子对角位置并给答案加一圈白色描边这样即使和题目交叠也能清楚地分离出来。这个处理在实际演示中效果非常好别人一眼就能分清印刷的题目和红色答案。如果你做的是视频流版本还可以把答案区域做成半透明浮层动态切换显示与隐藏。6. 项目复盘这些参数和思路值得存档6.1 我最终固定下来的参数参考表环节参数/操作取值说明缩放等比缩放宽高宽 600~900太大影响速度太小轮廓不稳定高斯滤波核大小5x5去除传感器噪声自适应阈值blockSize, C15, 2光照不均时明显优于全局阈值形态学闭运算核3x3连接断裂网格线一次即可外轮廓筛选多边形逼近 epsilon周长 x 2%偏小容易多出顶点偏大丢失直角校正输出尺寸450x450每格约 50x50识别器输入友好格线提取投影阈值最大值 x 0.5峰值超过中位值一半才认作格线数字模板模板尺寸32x32配合 matchTemplate 足够识别输入归一化尺寸48x48保留宽高比后居中填充空白判定黑像素占比 2% 判为空避免空格被误识别这些参数不是从哪本书抄来的而是反复试出来的。你换一批照片很可能需要微调但调整的方向和逻辑是一样的先看中间结果确定崩在哪一步再顺势调参。比如线条断了就搞形态学不要埋头加高斯核数字模糊就加大模板尺寸或者换网络不要盲目膨胀。6.2 从失败案例中学会的调试习惯这个项目我做了一半进度时识别率一直在 60% 徘徊每天改参数都像无头苍蝇。后来意识到一个问题我从来没把每一步的中间结果保存下来过。于是我在每个模块后加了一行cv2.imwrite把灰度图、二值图、轮廓图、校正图、格子切分图全部导出。导出之后20 秒就能定位到问题是出在轮廓提取还是在数字分类。这个习惯帮我节省了至少两天的改参数时间。强烈推荐你也这么做。不要省那几行代码中间结果是你调试时唯一的眼睛。尤其是轮廓绘制那一步把找到的四个角点点在原图上看看位置如果角点偏了后面一切都会跟着错。6.3 这个项目还能往哪些方向延伸实时视频识别手机或摄像头对准数独画面里实时叠加答案需要把单应矩阵每一帧重新计算或做平滑跟踪。手写数独把模板匹配换成更鲁棒的字符识别模型或者用手写数字增强数据重新训练。模糊/遮挡数独加入超分辨率或去阴影模块这是更接近真实场景的需求。边缘人像拍摄倾斜角度特别大时四边形的检测容易失效可以研究更鲁棒的角点检测方法。最后说点实践经验做完这个项目我最大的体会是计算机视觉不是一个“调库”的领域而是一个“理解像素如何流转”的领域。数独题目看起来简单但它把图像的获取、增强、分割、识别、几何变换、结果合成全部串起来每一步都对应一个经典问题。如果哪天有人问我入门 CV 做什么项目好我还是会推荐做一遍数独识别数据好获取、问题边界清晰、成就感来得快。最后分享一个小技巧先拿一张打印清晰、正对拍摄的“标准图”把整条流水线调通再拿一张随手拍的“地狱图”来打磨每个环节。这样做的好处是你永远知道问题出在哪个环节而不是在两张照片之间胡乱改参。这个思路也适用于很多其他视觉任务。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询