文章封面

欢迎关注QQ频道:电赛工坊

数字识别效果展示

话不多说,直接上图!👇
模板匹配数字识别效果
瞧瞧这效果!甭管待识别的数字是正着站、歪着躺,还是转着圈儿跳芭蕾,咱们的方法统统拿捏得死死的!是不是瞬间觉得手里的OpenMV突然没那么香了?别急,好戏还在后头——接下来,我们就手把手带你解锁这套神操作的完整流程!

模板匹配:原理与实现其实没那么复杂

想象一下这个场景:你手里拿着一张小小的模板图片,像玩“大家来找茬”一样,在一张大图上慢慢滑动,仔细比对每一个位置——没错,模板匹配的核心思想就是这么直白!

具体来说,你需要准备两张图:一张是待查找的“小模板”,另一张是“大源图”。匹配的过程,就是让这个小模板在源图上逐行逐列地滑动,并且在每一个停留的位置,都计算一下它和源图上对应区域到底有多像。

OpenCV 贴心地为我们准备好了cv2.matchTemplate函数,它的用法长这样:

result = cv2.matchTemplate(image, templ, method[, result[, mask]])

这个函数返回的结果非常有意思:它可不是一张普通的图片,而是一张神秘的“置信度地图”。它是一个单通道的32位浮点型矩阵。如果源图大小是 W × H,模板大小是 w × h,那结果矩阵的尺寸就是 (W - w + 1) × (H - h + 1)。这张“地图”上每一个点的值,都代表了一个惊人的事实:“如果以源图上这个位置为左上角,切一块和模板一样大的区域,那这块区域和模板的相似程度究竟有多少”。

匹配完成后,我们通常还会请出另一个好帮手:cv2.minMaxLoc。它能像雷达一样,快速帮我们扫描整张置信度地图,瞬间找出相似度的最高分、最低分以及它们所在的位置,堪称“寻宝小能手”。

在实际动手时,选择什么样的“比拼规则”(相似度算法)很重要。这里我们选用的是TM_CCOEFF_NORMED方法。它最大的好处就是会把匹配得分规整到**[0, 1]这个区间**,非常直观——你可以把它理解为“像不像的百分制考试”:得分越接近1,就说明“这俩简直是一个模子刻出来的!”;得分越低,那就……嗯,可能只是长得有点像的陌生人。

打造专属数字模板:从“全家福”到“个人证件照”

首先,咱们得准备一套标准的数字模板。聪明如我们,选择把0~9这十个数字先集体请到一张“全家福”里,然后再用图像处理技术,把它们一个个“抠”出来,变成独立的模板图片。
数字模板图片
具体怎么“抠”呢?请看下面的处理流程:

读取模板“全家福”并给它“美颜”一下

template_img = cv2.imread("/home/ertu/Shared/digital_templates.jpg")
ref_gray = cv2.cvtColor(template_img, cv2.COLOR_BGR2GRAY)
_, ref_thresh = cv2.threshold(ref_gray, 10, 255, cv2.THRESH_BINARY_INV)
ref_thresh = cv2.bitwise_not(ref_thresh)

这一步里的cv2.bitwise_not操作可是个关键角色!因为原始模板图片是黑底白字,经过二值化处理后,一不小心就变成了白底黑字。而咱们后面要请出的轮廓查找大佬——cv2.findContours,它有个固执的习惯:只在黑色背景里找白色物体的轮廓。所以,我们得用图像反转操作,把图像重新变成黑底白字,这样才能让轮廓识别顺利进行。

开始“抠图”——查找数字轮廓

contours, hierarchy = cv2.findContours(ref_thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

给每个数字办个“身份证”——提取并排序数字区域

为每个检测到的轮廓画个框,证明“这块地我占了”:

for cnt in contours:
    (x, y, w, h) = cv2.boundingRect(cnt)
    bounding_boxes.append((x, y, w, h))

然后按照每个框左上角x坐标的大小排排坐,确保数字顺序从0到9不乱套:

bounding_boxes = sorted(bounding_boxes, key=lambda b: b[0])

最后一步:制作标准“证件照”

现在,我们提取每个框里的数字区域并存起来。注意啦,为了提高后续识别的准确度和一致性,我们在存储前给所有数字模板统一了尺寸,就像给证件照规定尺寸一样:

for i, (x, y, w, h) in enumerate(bounding_boxes):
    roi = ref_thresh[y:y+h, x:x+w]
    roi_resized = cv2.resize(roi, (57, 88))
    digits[i] = roi_resized

处理并识别“规规矩矩”的数字:当数字不旋转时

我们先从最简单的情况开始——如何识别那些“站得笔直”、没有旋转角度的数字。这种情况实现起来相对轻松,核心思路就像玩“找相同”游戏:先从A4纸上的黑色矩形区域内把数字“抠”出来,然后和我们准备好的标准模板一个个比对,找出最像的那个。

关于区域提取部分,我们简要概括一下关键步骤(这部分就像给数字办入境手续):

首先,要定位A4纸的内边框,划出“特区”(ROI感兴趣区域);
接着,在特区内进行“人口普查”(轮廓检测),在每个小区域内仔细查找数字轮廓;
然后,给每个数字发个“标准身份证”(绘制最小外接矩形并裁剪),并且调整到和模板一样的大小;
最后,开始“人脸识别”(模板匹配),完成数字身份认证。

给数字“量体裁衣”

首先,把提取出来的数字图像缩放到和模板完全一致的尺寸,这是保证匹配准确性的前提——就像让所有人都站在同一根身高线前比较。

digit_resized = cv2.resize(digit_img, (57, 88))

“选美大赛”——寻找最佳匹配

现在开始一场数字选美!遍历所有数字模板,让每个模板都和待识别数字“同台竞技”,记录下相似度最高的那位以及它的得分。

for digit_value, template in digit_templates.items():
    result = cv2.matchTemplate(digit_resized, template, cv2.TM_CCOEFF_NORMED)
    _, max_val, _, _ = cv2.minMaxLoc(result)
            
    if max_val > best_score:
        best_score = max_val
        best_match = digit_value

处理并识别“放飞自我”的数字:当数字旋转时

接下来是本文的重头戏——如何对付那些“歪着脑袋”、带旋转角度的数字。前面的第一步操作和之前一样:先定位A4纸的内边框,划定我们的“侦察区域”(ROI)。但从这里开始,画风就完全不同了!

揪出内部各个边框的“最小外接矩形”

rect = cv2.minAreaRect(contour)

这里我们请出了神器cv2.minAreaRect。这个函数会返回一个RotatedRect对象,我们来拆解一下它带回的情报:

(cx, cy), (w_rect, h_rect), angle = rect
  • (cx, cy):最小外接矩形的中心点坐标,相当于找到了这个旋转矩形的“心脏”;
  • (w_rect, h_rect):矩形的宽和高,就是它的“身材尺寸”;
  • angle:旋转角度,告诉我们它到底“歪”了多少度——这是后续给它“扶正”的关键!

制作“扶正”指南:构建仿射变换矩阵

rotation_matrix = cv2.getRotationMatrix2D((black_rect_region.shape[1]//2, black_rect_region.shape[0]//2), angle, 1.0)

我们调用cv2.getRotationMatrix2D函数来生成一个仿射变换矩阵。输入参数包括旋转中心(这里我们用的是图像中心)、旋转角度(就是上一步测出的那个angle)和缩放比例。这就好比给旋转的数字制定了一份“如何站直”的详细说明书。

开始“扶正”操作:执行仿射变换

接下来,我们使用cv2.warpAffine函数,根据刚才生成的“说明书”,对图像进行仿射变换,把歪斜的图像“摆正”。

rotated_black_rect_region = cv2.warpAffine(black_rect_region, rotation_matrix, (black_rect_region.shape[1], black_rect_region.shape[0]), 
                                               flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_CONSTANT, borderValue=(255, 255, 255))                       

图像一旦被我们“扶正”之后,后面的流程就是熟悉的模板匹配了,这里就不再重复啦——相信你们已经轻车熟路了!

旋转数字识别避坑指南:少走弯路的经验分享

搞懂 cv2.minAreaRect 的“脾气”:返回角度与宽高定义

正确理解cv2.minAreaRect函数的返回角度,是后续仿射变换能否成功的关键第一步。笔者最初就在这里栽过跟头,下面一次给大家讲清楚这个函数返回的角度和宽高到底是怎么定义的。

首先要注意,cv2.minAreaRect()返回的角度范围因OpenCV版本而异

  • OpenCV 4.5 之前版本(如4.2、4.3、4.4):角度范围为 [-90°, 0°)
  • OpenCV 4.5 及之后版本(目前主流):角度范围为 [0°, 90°]
    (笔者使用的就是4.5以后的版本)

具体定义如下:

  • 角度定义:从 x 轴正方向开始,顺时针旋转,第一次碰到矩形边时转过的角度,就是这个返回的角度值。
  • 宽高定义:与这个返回角度对应的那条边被定义为 width,另一条边则为 height。注意:width 不一定比 height 长
  • 一个隐藏机制:如果原始角度大于90°,函数会自动将 width 和 height 对调,并用原角度减去90°的值作为返回角度,以此来保证返回值落在规定的角度范围内。

OpenCV几何变换中的“左右手定则”:角度正负的定义

搞懂了 cv2.minAreaRect 的返回角度,接下来要进行仿射变换,具体该转多少度呢?这就需要了解OpenCV几何变换中关于角度正负的“潜规则”:

正角度值 = 逆时针旋转(左转)
负角度值 = 顺时针旋转(右转)

如何确定往哪边转?一个实用的“试错法”

仅仅根据 cv2.minAreaRect() 返回的信息,是无法直接确定旋转方向的。到底该往哪边转,需要我们自己判断。笔者这里分享一个自认为比较巧妙的方法:

我们先统一按照一个方向进行旋转(比如都逆时针旋转|angle|度)。这样旋转后无非会出现两种情况:

  • 图像被正确摆正了(皆大欢喜!)
  • 图像相较于正确摆正的情况,额外逆时针旋转了90°(有点歪)

那么如何判断旋转是否正确呢?我们在提取数字时,会绘制数字轮廓的外接矩形。只需要判断一下这个外接矩形的宽高关系:

  • 如果数字区域正确摆正,它的高度通常大于宽度(数字一般是“瘦高个”)
  • 如果发现宽度反而大于高度,说明我们需要再给它“拧”一下
if w_d > h_d:
    digit_region = cv2.rotate(digit_region, cv2.ROTATE_90_CLOCKWISE)                           

通过这个简单的宽高判断,我们就能确保数字最终以正确的姿态进入识别环节,大大提高识别准确率。


©️ 版权声明 | 📮 合作咨询 | 🚑 急救资源

原创内容:@二土电子团队

免责声明:本教程仅供学习参考

未经授权禁止任何形式转载/商用
Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐