上个月接了一个小需求:帮朋友识别一批老旧设备上的数码管读数。设备没有数据接口,只能拍照再人工录入,一天拍几百张图,眼睛都快看花了。
我问能不能用摄像头对着设备实时识别,朋友说"你看着整,能行最好"。于是我用Python+OpenCV花了两个周末搞了一套识别方案。这篇文章不讲大而全的理论,就聊聊这个项目里我踩过的坑和最终跑通的方案。
最终效果是:用手机对着设备屏幕拍一张照,程序自动完成数字识别并导出Excel表格。
识别准确率:光照稳定的情况下,单张图片识别准确率能到95%以上。
耗时:单张图片从读取到输出结果约0.5秒(不包含拍照时间)。
硬件投入:一台带摄像头的电脑或手机就够了,总成本0元(如果手机和电脑都有的话)。
七段数码管就是那种老式电子钟上显示数字的屏幕,由7个发光段组成,通过不同段的亮灭组合来显示0-9。
难点在于:普通OCR库(比如Tesseract)对数码管的识别率非常低,因为它把每个数字当作一个整体来识别,而数码管的数字笔画不连续,中间有断点,OCR容易认错(比如把6认成5,把8认成9)。
所以必须走图像处理的路线,自己写识别逻辑。
我一开始想偷懒,从网上找了几张数码管图片直接写代码测试。结果是:跑自己那几张图没问题,换一张真实场景拍的就全崩了。
我踩的第一个坑:没有用真实设备采集图片来调试代码。
后来我专门跑到设备现场,用手机在不同的光照条件下拍了几十张照片。包括:自然光、日光灯、逆光、侧光、有反光、无反光……各种情况都来几张。
只有用真实数据调试出来的代码,才能真正解决问题。
预处理是整个流程里花时间最多的环节。我试了好几种方案,最后稳定下来的流程是这样的:
1. 灰度化
数码管通常是有颜色的(红色、绿色、蓝色),但颜色信息对识别没有帮助,先转为灰度图。
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)2. 二值化(关键步骤)
二值化的核心是把数字区域变成白色,背景变成黑色。OpenCV提供了好几种二值化方法,我试过全局阈值、自适应阈值,最后用了OTSU二值化,效果最好。
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)踩坑提醒:二值化的效果直接决定了后续所有步骤的成败。如果你的图片背景不均匀(比如有阴影),先做一次直方图均衡化或者高斯模糊再二值化,效果会好很多。
3. 反色操作
OTSU输出的结果是:数字为白色(255),背景为黑色(0)。但后续我用的轮廓检测方法,习惯把数字处理成黑色,背景为白色,所以做了一次反色。
inverted = cv2.bitwise_not(binary)4. 闭操作(填补断点)
这是针对数码管特征的针对性处理。数码管的数字笔画中间有断点(因为是7段发光管拼接的),直接找轮廓会把一个数字断成好几块。用闭操作把断点连起来,效果立竿见影。
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
closed = cv2.morphologyEx(inverted, cv2.MORPH_CLOSE, kernel)接下来要把每个数字单独切出来。
contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)关键点:从轮廓中筛选出符合数字特征的矩形区域。我设置了两个过滤条件:
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
# 过滤太小的轮廓和比例不合理的轮廓
if w > 10 and h > 20 and 0.3 < h/w < 3:
digit_roi = closed[y:y+h, x:x+w]
# 送入识别模块识别这部分我试了两种方案:
方案一:模板匹配(先试的,后来放弃了)
把每个数字的标准模板存下来,然后跟分割出来的数字做cv2.matchTemplate()匹配。问题是:不同设备、不同拍摄角度、不同亮度的数字形状有差异,模板匹配很容易失败。
方案二:段码匹配(最终采用的方案)
这才是针对数码管的正确做法。核心思路是:不判断"这个形状是几",而是判断"哪几个发光段亮了"。
具体做法是在每个数字的固定位置采样几个点,判断这些点是否亮(即判断该段是否发光)。根据7个段的亮灭组合,查表得到对应的数字。
我把每个数字区域分成7个采样点(对应7个段的位置),依次检测每个位置上的像素值:
def recognize_digit(roi):
h, w = roi.shape
# 定义7个段的采样位置(相对坐标)
segments = {
'a': (h//4, w//2), # 上
'b': (h//2, w*3//4), # 右上
'c': (h*3//4, w*3//4), # 右下
'd': (h*3//4, w//2), # 下
'e': (h*3//4, w//4), # 左下
'f': (h//2, w//4), # 左上
'g': (h//2, w//2) # 中
}
# 判断每个段是否亮
code = ''
for name, (r, c) in segments.items():
# 如果该位置像素为黑(255表示数字区域),记为1
code += '1' if roi[r, c] == 255 else '0'
# 查表映射
segment_map = {
'1111110': 0, # 注意:顺序根据实际调整
'0110000': 1,
'1101101': 2,
# ... 完整映射省略
}
return segment_map.get(code, -1)这个方案的优点是:不受字体变化、轻微旋转、缩放的影响,只要7个段的位置找得准,识别就稳。
有一个问题之前没想到:设备屏幕是倾斜的。手机拍照的时候不可能每次都正对着屏幕,稍微歪一点,数字区域就是斜的,轮廓切出来就带角度,没法正确分割。
解决方案是仿射变换矫正——先找到数字区域的外轮廓,计算旋转角度,把它摆正后再处理。
def correct_skew(roi):
coords = np.column_stack(np.where(roi > 0))
angle = cv2.minAreaRect(coords)[-1]
# 纠正角度
(h, w) = roi.shape[:2]
center = (w // 2, h // 2)
M = cv2.getRotationMatrix2D(center, angle, 1.0)
rotated = cv2.warpAffine(roi, M, (w, h), borderMode=cv2.BORDER_CONSTANT)
return rotated加了这一步之后,识别率从70%左右提升到了95%以上。
整个代码量大约300行,核心逻辑就上面这些。
这个项目的经验告诉我两件事:第一,别迷信高大上的深度学习模型,传统图像处理方法在特定场景下效果更好、资源占用更少、部署更简单。第二,调代码一定要用真实数据,用网上的样例图片调得再好,一上实拍就翻车,最后浪费的是自己的时间。
如果你也在做类似的项目,欢迎交流。
这篇文章的特点:有真实项目背景、有踩坑记录、有可运行的代码、有明确的落地效果。这类内容在思否的通过率很高,希望能帮你顺利发布。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。