☰
Python+OpenCV实战:LED数码管电表读数自动识别技术详解
2026/9/27 23:51:39 网站建设 项目流程

简介:本资源面向Python初学者与计算机视觉入门开发者,提供一套基于OpenCV的LED数字电表读数自动识别完整实现方案,解决电力监控、智能抄表等场景中人工读数效率低、易出错的问题。压缩包共2个文件(1个核心Python脚本+1张效果演示图),总大小仅205KB,轻量易部署;其中image_re.py封装了图像灰度化、二值化、轮廓检测、形态学优化及模板匹配识别全流程,代码结构清晰、注释充分,便于理解LED段码特征与OpenCV图像处理链路;效果图.png直观展示原始电表图像与识别结果对比,验证方案可行性。已有1817人学习下载,读者可直接运行调试、迁移至嵌入式边缘设备或扩展为多表并行识别系统,亦可作为OpenCV图像预处理与数字模式识别的教学范例。

1. 项目缘起:从人工抄表到智能识别的痛点

前阵子有个朋友在搞一个老旧小区的能耗监测项目,需要定期采集几百块电表的读数。他跟我吐槽,说每天派个人拿着本子去抄,不仅效率低,还容易看错,特别是那种老式的LED数码管电表,数字小、反光、角度偏一点就看不清,回来录入电脑又是一堆麻烦。他问我能不能用技术手段解决一下,比如拍个照片自动把读数识别出来。

我一听,这不就是典型的计算机视觉OCR(光学字符识别)任务吗?不过,和识别打印体或者手写体不同,电表上的LED数字有它自己的特点:通常是7段数码管(或者14段、16段)显示,由发光的段组合成数字;背景可能是黑色、深灰色;拍摄环境的光照、角度、遮挡都可能不理想。直接用通用的OCR库(比如Tesseract)效果往往很差,因为通用OCR模型是针对自然场景下的字符训练的,对LED这种点阵、高对比度、特定字体风格的字符识别率不高。

所以,这个项目的核心就变成了:如何针对“LED数码管数字”这一特定目标,设计一个从图像预处理、数字定位、分割到最终识别的完整流程。Python + OpenCV 这个组合就非常合适了。OpenCV提供了强大的图像处理工具链,而Python则能让我们快速搭建原型、验证算法。最终的目标是给出一套稳定、可复现的解决方案,并且附上可以直接跑起来的源代码。

2. 核心思路拆解:为什么通用OCR行不通?

在动手写代码之前,我们必须先想清楚技术路线。很多人第一个想法是调百度、阿里云的OCR API,或者用开源的PaddleOCR、EasyOCR。这些方案对于文档、车牌等场景确实强大,但在我们这种特定场景下,可能会遇到几个致命问题:

  1. 误识别率高:LED数字的“1”可能被识别成“I”或“l”,断开的“8”可能被识别成“0”或“6”,通用模型没有针对这种字体进行优化。
  2. 定位困难:电表图像可能包含表盘、铭牌、按钮等多种信息,通用OCR需要先进行文本检测,在复杂背景下可能无法准确框出我们关心的读数区域。
  3. 成本与依赖:使用云API会产生费用,并且需要网络。对于本地化、离线部署的应用场景(比如嵌入式设备)不友好。
  4. 定制化差:当电表型号更换,LED显示样式(颜色、粗细、间距)发生变化时,通用模型难以快速适配。

因此,我们决定采用传统图像处理 + 定制化模板匹配/特征识别的路线。这条路线的优势在于:

  • 原理透明,可控性强:每一步处理(滤波、二值化、轮廓查找)的结果都肉眼可见,出了问题容易调试。
  • 不依赖网络和大型模型:完全本地运行,速度快,资源消耗低。
  • 高度定制化:可以根据具体的电表样式调整每一个处理参数,达到最优识别效果。

我们的核心处理流程可以概括为四个阶段:

  1. 图像预处理:把拍到的彩色/灰度图,处理成干净、高对比度的二值图(黑白图),让LED数字区域“凸显”出来。
  2. 数字区域定位与分割:找到图像中所有可能是数字的区域,并把它们一个个“抠出来”。
  3. 数字识别:对每一个抠出来的数字子图,判断它是0-9中的哪一个。
  4. 结果输出:将识别出的单个数字按顺序组合成最终读数。

下面,我们就沿着这个流程,一步步拆解其中的技术细节和实操代码。

3. 实战第一步:图像预处理——把“信号”从“噪声”中提取出来

拿到一张电表照片,我们直接进行识别是不现实的。背景杂乱、光照不均、镜头污渍、数字本身亮度不足等问题都会干扰后续步骤。预处理的目标就是最大限度地保留数字信息,同时抑制无关噪声。

3.1 读取与灰度化

第一步永远是读图。OpenCV的imread函数默认以BGR格式读取彩色图像,但我们后续处理大多在灰度空间进行,因为颜色信息对识别LED数字帮助不大,反而增加计算量。

import cv2 import numpy as np # 读取图像 image_path = "meter.jpg" image = cv2.imread(image_path) if image is None: print(f"错误:无法读取图像 {image_path}") exit() # 转换为灰度图 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

注意:OpenCV的默认颜色通道顺序是BGR,而不是常见的RGB。如果后续需要显示,用matplotlib的话要注意转换,但处理阶段用灰度图无需担心。

3.2 对比度增强与光照补偿

拍摄环境的光照可能不均匀,导致图像一部分亮一部分暗。我们可以使用CLAHE(限制对比度自适应直方图均衡化)来改善局部对比度,这对处理LED发光区域特别有效,能让暗淡的数字变清晰。

# 创建CLAHE对象,设置对比度限制和网格大小 clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) gray_clahe = clahe.apply(gray)

clipLimit参数控制对比度限制,值越大,对比度越强,但噪声也可能被放大。tileGridSize定义了图像被分成多少个小块进行局部均衡化。这两个参数需要根据具体图像调整。

3.3 二值化:关键中的关键

这是预处理中最核心的一步,目的是将图像转换为纯粹的黑白两色,前景(数字)为白色,背景为黑色。LED数字通常是自身发光,在较暗背景下呈现高亮,因此适合使用阈值分割。

全局阈值简单但脆弱,因为光照不均会导致部分区域阈值不适用。更鲁棒的方法是自适应阈值。

# 方法1:自适应阈值(常用) # 参数:源图像,最大阈值,自适应方法,阈值类型,邻域块大小,常数C binary = cv2.adaptiveThreshold(gray_clahe, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2) # 或者使用均值法 # binary = cv2.adaptiveThreshold(gray_clahe, 255, cv2.ADAPTIVE_THRESH_MEAN_C, # cv2.THRESH_BINARY, 11, 2) # 方法2:大津法(Otsu‘s)全局阈值(在直方图双峰明显时效果好) # _, binary_otsu = cv2.threshold(gray_clahe, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
  • cv2.ADAPTIVE_THRESH_GAUSSIAN_C:根据像素邻域的高斯加权和确定阈值,抗噪性好。
  • blockSize:邻域大小,必须是奇数。它决定了局部区域的面积,值太小会对噪声敏感,太大则可能模糊边界。对于电表图像,数字尺寸不大,11或15是不错的起点。
  • C:从计算出的均值或加权均值中减去的常数,用于微调。可以是正数或负数,用于让阈值更宽松或更严格。

实操心得:adaptiveThreshold的blockSize和C是需要反复调试的“魔法参数”。我的经验是,先用一个较大的blockSize(如31)观察整体效果,如果数字内部出现空洞(本该白的地方变黑),就减小blockSize或减小C值。可以写一个简单的带滑块的GUI来实时调整,直观感受参数影响。

3.4 形态学操作:修补与净化

二值化后的图像,数字笔画可能因为拍摄模糊或阈值问题出现断裂,背景也可能有一些小的噪声点。这时就需要用到形态学操作。

  • 闭运算(先膨胀后腐蚀):用于连接断裂的笔画。比如LED数字“8”中间断开,可能被识别成两个圈,闭运算可以将其连接。
  • 开运算(先腐蚀后膨胀):用于消除小的白色噪声点。
  • 膨胀/腐蚀:单独使用可以加粗或细化笔画。
# 定义一个核(结构元素),这里用矩形核 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) # 闭运算,连接断点 binary_closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # 开运算,去除小噪点(如果噪声多的话) binary_cleaned = cv2.morphologyEx(binary_closed, cv2.MORPH_OPEN, kernel)

注意:形态学操作是一把双刃剑。过度使用闭运算可能导致相邻数字粘连在一起(比如“1”和“8”变成一块),这将给后续的数字分割带来灾难。所以核的大小((3,3), (5,5))需要谨慎选择,原则是“宁小勿大”,优先保证数字的独立性。

预处理完成后,我们应该得到一张背景为黑、数字为白,且笔画连贯、噪声较少的二值图像。这是后续所有操作的基础。

4. 实战第二步:数字定位与分割——找到并“抠出”每一个数字

现在我们有了一张干净的二值图,下一步就是找到图中所有像数字的白色区域,并把它们单独提取出来。

4.1 轮廓查找与筛选

OpenCV的findContours函数可以找到图像中所有的白色轮廓。

# 注意:findContours会修改源图像,通常使用副本 binary_image = binary_cleaned.copy() # 查找轮廓。RETR_EXTERNAL只取最外层轮廓,CHAIN_APPROX_SIMPLE压缩轮廓点 contours, hierarchy = cv2.findContours(binary_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

找到的轮廓有很多,可能是数字,也可能是污渍、螺丝孔、边框等。我们需要根据先验知识进行筛选。LED数字通常具有以下特征:

  1. 宽高比:单个数字的轮廓 bounding rect 的宽高比有一定范围,比如在0.2到1.2之间(“1”比较瘦长,“0”比较宽)。
  2. 面积:面积不能太小(排除噪声点),也不能太大(排除整个表盘)。
  3. 占空比:轮廓面积与其外接矩形面积的比值(即填充率),数字的填充率不会太低,也不会是实心矩形。
  4. 位置:多个数字通常在同一水平线上,且间距大致相等。
digit_contours = [] image_height, image_width = binary_image.shape for cnt in contours: # 获取轮廓的外接矩形 x, y, w, h = cv2.boundingRect(cnt) # 筛选条件1:排除太小的轮廓(可能是噪声) min_area = 100 # 根据图像分辨率调整 if cv2.contourArea(cnt) < min_area: continue # 筛选条件2:排除太扁或太高的轮廓(可能是横线或竖线干扰) aspect_ratio = w / float(h) if aspect_ratio < 0.15 or aspect_ratio > 1.5: # “1”的宽高比可能小于0.3 continue # 筛选条件3:高度不能太小,确保是有效字符 if h < 20: # 根据图像中数字的实际像素高度调整 continue # 筛选条件4:通过填充率进一步筛选(可选) area = cv2.contourArea(cnt) rect_area = w * h fullness = area / rect_area if rect_area > 0 else 0 if fullness < 0.2 or fullness > 0.95: # 太稀疏或太实心都可能不是数字 continue digit_contours.append((x, y, w, h, cnt)) # 保存矩形信息和轮廓本身

这些阈值参数(min_area,aspect_ratio,h,fullness)是需要针对你的具体电表图片进行校准的。最好的方法是打印出所有轮廓的这些属性,观察数字轮廓和非数字轮廓的分布,然后确定一个合理的范围。

4.2 轮廓排序与校正

筛选出的数字轮廓在列表里可能是乱序的(按发现顺序)。我们需要按照从左到右的顺序排列,才能组成正确的读数。

# 按轮廓的左上角x坐标排序 digit_contours_sorted = sorted(digit_contours, key=lambda item: item[0])

更复杂的情况是,数字可能不是完全水平的。这时可以先对所有轮廓的中心点(x + w//2, y + h//2)进行拟合,如果发现明显的倾斜,可以对整个图像或每个ROI进行旋转校正。但对于大多数正面拍摄的照片,这步可以省略。

4.3 提取数字ROI并标准化

现在,我们有了有序的、经过筛选的轮廓矩形框。接下来就是把每个框里的数字“抠出来”,并统一成相同大小,便于后续识别。

digits = [] for (x, y, w, h, cnt) in digit_contours_sorted: # 从原始二值图上截取ROI roi = binary_cleaned[y:y+h, x:x+w] # 标准化:统一缩放到固定大小,比如28x28(类似MNIST数据集) # 首先计算需要添加的边框,使图像居中 if h > w: # 高度大于宽度,在左右两侧加边框 padding = (h - w) // 2 roi = cv2.copyMakeBorder(roi, 0, 0, padding, padding, cv2.BORDER_CONSTANT, value=0) else: # 宽度大于高度,在上下两侧加边框 padding = (w - h) // 2 roi = cv2.copyMakeBorder(roi, padding, padding, 0, 0, cv2.BORDER_CONSTANT, value=0) # 缩放到目标尺寸 target_size = (28, 28) roi_resized = cv2.resize(roi, target_size, interpolation=cv2.INTER_AREA) # 可以添加一个简单的二值化,确保是纯黑白(因为resize可能产生灰度) _, roi_final = cv2.threshold(roi_resized, 127, 255, cv2.THRESH_BINARY) digits.append(roi_final) # 可视化:在原图上画出矩形框 cv2.rectangle(image, (x, y), (x+w, y+h), (0, 255, 0), 2)

这一步的标准化非常重要。无论原始图像中数字有多大,我们都将其转换为统一尺寸(如28x28),这样后续无论是模板匹配还是神经网络,输入都是一致的。

5. 实战第三步:数字识别——从像素到数字

这是最后一步,也是决定成败的一步。我们有几种主流方法:

5.1 方法一:模板匹配法

原理:预先制作好0-9这10个数字的标准模板(同样是28x28的二值图)。然后将待识别的数字ROI与每个模板进行相似度计算,取相似度最高的作为结果。

优点:简单、快速、无需训练、可解释性强。缺点:对数字的旋转、缩放、笔画粗细变化敏感。如果电表LED的字体风格与模板差异大,识别率会下降。

def recognize_by_template(digit_img, templates): """ digit_img: 待识别的数字图像(28x28, 0-255) templates: 字典,key为数字字符,value为对应的模板图像 """ best_match = None best_score = -1 # 将待识别图像二值化并展平为1维向量 _, digit_bin = cv2.threshold(digit_img, 127, 255, cv2.THRESH_BINARY) digit_flat = digit_bin.flatten().astype(np.float32) for digit_char, template_img in templates.items(): # 模板也需处理成同样格式 _, temp_bin = cv2.threshold(template_img, 127, 255, cv2.THRESH_BINARY) temp_flat = temp_bin.flatten().astype(np.float32) # 计算余弦相似度(或相关系数) # 使用点积计算余弦相似度 score = np.dot(digit_flat, temp_flat) / (np.linalg.norm(digit_flat) * np.linalg.norm(temp_flat) + 1e-8) if score > best_score: best_score = score best_match = digit_char return best_match, best_score # 假设我们已经加载了0-9的模板图像到字典templates中 # recognized_digits = [] # for digit in digits: # char, score = recognize_by_template(digit, templates) # recognized_digits.append(char)

如何制作模板?最可靠的方法是从你实际要识别的电表图片中,手动裁剪出标准的0-9数字,然后标准化到28x28。这样可以保证字体、笔画粗细完全一致。也可以使用标准字体(如LCD、7段数码管字体)生成,但匹配度会差一些。

5.2 方法二:特征提取 + 分类器

原理:从数字图像中提取一些人工设计的特征(如HOG方向梯度直方图、LBP局部二值模式、轮廓特征、投影特征等),然后使用一个简单的分类器(如SVM、KNN)进行训练和预测。

优点:比模板匹配更鲁棒,能容忍一定的形变。缺点:需要收集标注数据并进行训练,特征设计需要一些经验。

import cv2 import numpy as np from sklearn import svm from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score # 假设我们有一个数据集:X是特征向量列表,y是对应的标签(0-9) # 1. 提取HOG特征 def extract_hog_features(image): # 图像需要是二值图 winSize = (28, 28) blockSize = (14, 14) blockStride = (7, 7) cellSize = (7, 7) nbins = 9 hog = cv2.HOGDescriptor(winSize, blockSize, blockStride, cellSize, nbins) hist = hog.compute(image) return hist.flatten() # 2. 准备数据 # X = [extract_hog_features(img) for img in all_digit_images] # y = corresponding_labels # 3. 划分训练集和测试集 # X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) # 4. 训练SVM分类器 # clf = svm.SVC(kernel='linear', C=1.0) # clf.fit(X_train, y_train) # 5. 预测 # y_pred = clf.predict(X_test) # print(f"准确率: {accuracy_score(y_test, y_pred)}")

5.3 方法三:卷积神经网络(CNN)

原理:使用深度学习模型,让网络自动从图像中学习特征并进行分类。这是目前最强大、最鲁棒的方法。

优点:识别准确率高,对光照、角度、轻微形变、不同字体风格有很好的适应性。缺点:需要较多的标注数据,训练需要GPU和时间,模型部署比前两者稍复杂。

对于这个项目,我们可以使用一个非常简单的CNN模型,结构类似于LeNet-5。

import tensorflow as tf from tensorflow import keras from tensorflow.keras import layers def create_simple_cnn(input_shape=(28, 28, 1), num_classes=10): model = keras.Sequential([ layers.Conv2D(32, (3, 3), activation='relu', input_shape=input_shape), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activation='relu'), layers.MaxPooling2D((2, 2)), layers.Flatten(), layers.Dense(128, activation='relu'), layers.Dropout(0.5), layers.Dense(num_classes, activation='softmax') ]) model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy']) return model # 数据准备:需要将digits列表转换为numpy数组,并增加一个通道维度 # X_train = np.array(digits_train).reshape(-1, 28, 28, 1) / 255.0 # y_train = np.array(labels_train) # ... 类似处理测试集 # 创建并训练模型 # model = create_simple_cnn() # model.fit(X_train, y_train, epochs=10, validation_data=(X_test, y_test)) # 使用模型预测单个数字 # digit_for_pred = digits[0].reshape(1, 28, 28, 1) / 255.0 # prediction = model.predict(digit_for_pred) # recognized_digit = np.argmax(prediction)

实操心得:对于电表读数这种特定场景,你不需要ImageNet那样庞大的数据集。可以从实际拍摄的几百张电表图片中,手动标注出几千个数字(一个电表通常有5-8位数字),就足够训练一个效果非常好的小模型了。数据增强(旋转、缩放、平移、添加噪声)可以显著提升模型的泛化能力。

5.4 方法选择与实战建议

对于刚入门或者想快速验证的开发者,我推荐模板匹配法。它的实现最简单,只要你能制作出与目标电表字体高度一致的模板,识别率可以非常高。而且整个过程透明,调试方便。

当你需要处理多种不同样式的电表,或者模板匹配效果不稳定时,可以考虑SVM+HOG的方法。它比模板匹配更鲁棒,又比CNN简单。

如果你的项目对准确率要求极高,或者需要部署在多种复杂环境下(不同品牌、不同型号的电表),那么投入时间收集数据、训练一个轻量级CNN模型是值得的。训练好的模型可以保存为.h5或.tflite文件,在Python甚至移动端、嵌入式设备上都能方便地调用。

6. 避坑指南与性能优化

在实际部署中,你会遇到很多在理想测试图片中遇不到的问题。下面分享几个我踩过的坑和解决方案。

6.1 数字粘连与断裂的处理

  • 问题:预处理时闭运算过度,或者原始图像中数字间距太小,导致两个数字的轮廓被连成一个。
  • 排查:在画出轮廓矩形后,观察矩形的宽度。如果某个矩形的宽度明显是其他数字的2倍左右,很可能包含了两个数字。
  • 解决:
    1. 调整预处理:减小闭运算的核大小,或者尝试不使用闭运算,改用其他方法(如中值滤波)去除噪声。
    2. 轮廓后处理:对过宽的轮廓,计算其X方向的投影直方图。在两个数字之间,投影值会有一个明显的低谷。可以尝试在这个低谷处进行分割。
    3. 使用更精细的检测方法:可以考虑使用连通组件分析,并结合数字的预期宽高比进行筛选和分割。

6.2 复杂背景与干扰项的过滤

  • 问题:电表玻璃反光、金属边框、螺丝、其他文字(如“kWh”)被误识别为数字。
  • 排查:打印出所有被筛选出的轮廓及其属性(面积、宽高比、位置)。
  • 解决:
    1. 位置先验:如果知道数字大致出现在图像的某个区域(如中间偏右),可以设置一个ROI(感兴趣区域),只在这个区域内查找轮廓。
    2. 多级筛选:除了面积和宽高比,可以加入轮廓的层次关系(hierarchy)筛选,排除被嵌套的轮廓(可能是数字内部空洞,也可能是其他结构)。还可以利用数字的排列规律:多个有效数字的Y坐标(顶部)应该大致相同,高度也相近。可以对筛选出的轮廓进行聚类,只保留最大的、符合水平排列规律的那一组。

6.3 识别置信度与结果校验

  • 问题:某个数字识别错了,导致整个读数错误。
  • 解决:
    1. 设置置信度阈值:无论是模板匹配的相似度得分,还是CNN预测的softmax概率,都设定一个阈值(比如0.7)。低于阈值的识别结果标记为“不确定”,可以触发报警(如记录日志、要求人工复核),而不是直接使用。
    2. 业务逻辑校验:电表读数通常有范围。比如,新装电表读数不可能为999999,两次抄表间的读数差不会离谱地大。可以在输出最终结果前,加入这类简单的业务规则校验。
    3. 多帧投票:对于视频流或连续拍摄的多张图片,可以对同一块电表识别多次,然后对每一位数字进行投票,取出现次数最多的结果,可以有效抑制单次识别的偶然错误。

6.4 性能优化技巧

如果需要在树莓派等资源受限的设备上运行,可以采取以下优化:

  1. 降低图像分辨率:在保证数字清晰可辨的前提下,将输入图像缩放至较小的尺寸(如640x480),能大幅减少处理时间。
  2. 固定ROI:如果摄像头和电表位置固定,可以预先标定好数字出现的像素区域,后续只处理这个区域,避免全图搜索。
  3. 选择轻量级识别方法:模板匹配的速度远快于CNN。在满足准确率要求的前提下优先使用。
  4. 使用C++或优化库:如果Python版本速度不达标,可以考虑用C++重写核心算法,或者使用OpenCV的UMat(使用OpenCL加速)。

7. 完整源代码框架与使用示例

将上述所有步骤整合,一个完整的、可运行的示例代码如下。这里以模板匹配法为例,因为它最易于理解和复现。

import cv2 import numpy as np import os class LEDMeterReader: def __init__(self, template_dir): """初始化,加载数字模板""" self.templates = {} # 假设模板图片命名为 0.png, 1.png ... 9.png for i in range(10): path = os.path.join(template_dir, f"{i}.png") if os.path.exists(path): img = cv2.imread(path, cv2.IMREAD_GRAYSCALE) _, img_bin = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY) # 统一缩放到28x28 img_resized = cv2.resize(img_bin, (28, 28), interpolation=cv2.INTER_AREA) self.templates[str(i)] = img_resized else: print(f"警告:模板文件 {path} 不存在。") def preprocess(self, image): """图像预处理""" # 灰度化 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # CLAHE增强对比度 clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) enhanced = clahe.apply(gray) # 自适应阈值二值化 binary = cv2.adaptiveThreshold(enhanced, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2) # 形态学闭运算,连接断点(谨慎使用) kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2)) closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) return closed def find_and_sort_digits(self, binary_image): """查找并排序数字轮廓""" contours, _ = cv2.findContours(binary_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) digit_boxes = [] # 存储(x, y, w, h, contour) h_img, w_img = binary_image.shape for cnt in contours: x, y, w, h = cv2.boundingRect(cnt) area = cv2.contourArea(cnt) # 筛选条件 if area < 80 or h < 15 or w < 5: continue aspect_ratio = w / float(h) if aspect_ratio > 1.5 or aspect_ratio < 0.15: continue # 简单的位置过滤:假设数字在图像中间2/3宽度区域 if x < w_img * 0.2 or x > w_img * 0.8: continue digit_boxes.append((x, y, w, h, cnt)) # 按x坐标从左到右排序 digit_boxes.sort(key=lambda b: b[0]) return digit_boxes def extract_digit_image(self, binary_image, box): """从二值图中提取单个数字图像并标准化""" x, y, w, h, _ = box roi = binary_image[y:y+h, x:x+w] # 添加边框使其成为正方形 diff = abs(h - w) pad1, pad2 = diff // 2, diff - diff // 2 if h > w: roi = cv2.copyMakeBorder(roi, 0, 0, pad1, pad2, cv2.BORDER_CONSTANT, value=0) else: roi = cv2.copyMakeBorder(roi, pad1, pad2, 0, 0, cv2.BORDER_CONSTANT, value=0) # 缩放到28x28 roi = cv2.resize(roi, (28, 28), interpolation=cv2.INTER_AREA) _, roi = cv2.threshold(roi, 127, 255, cv2.THRESH_BINARY) return roi def recognize_digit(self, digit_img): """使用模板匹配识别单个数字""" best_match = '?' best_score = -1 _, digit_bin = cv2.threshold(digit_img, 127, 255, cv2.THRESH_BINARY) digit_flat = digit_bin.flatten().astype(np.float32) for digit_char, template_img in self.templates.items(): _, temp_bin = cv2.threshold(template_img, 127, 255, cv2.THRESH_BINARY) temp_flat = temp_bin.flatten().astype(np.float32) # 计算余弦相似度 score = np.dot(digit_flat, temp_flat) / (np.linalg.norm(digit_flat) * np.linalg.norm(temp_flat) + 1e-8) if score > best_score: best_score = score best_match = digit_char # 可以设置一个置信度阈值,例如0.6 return best_match if best_score > 0.6 else '?' def read_meter(self, image_path, visualize=False): """主函数:读取电表图像并返回识别结果""" image = cv2.imread(image_path) if image is None: print(f"无法读取图像: {image_path}") return None # 1. 预处理 binary = self.preprocess(image) # 2. 定位数字 digit_boxes = self.find_and_sort_digits(binary) if not digit_boxes: print("未找到数字区域") return None # 3. 提取并识别 result_digits = [] for box in digit_boxes: digit_img = self.extract_digit_image(binary, box) digit_char = self.recognize_digit(digit_img) result_digits.append(digit_char) if visualize: x, y, w, h, _ = box cv2.rectangle(image, (x, y), (x+w, y+h), (0, 255, 0), 2) cv2.putText(image, digit_char, (x, y-5), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2) if visualize: cv2.imshow("Detection Result", image) cv2.waitKey(0) cv2.destroyAllWindows() return ''.join(result_digits) # 使用示例 if __name__ == "__main__": # 初始化,传入存放0-9模板图片的文件夹路径 reader = LEDMeterReader(template_dir="./digit_templates") # 识别单张图片 image_path = "your_meter_photo.jpg" reading = reader.read_meter(image_path, visualize=True) if reading: print(f"识别到的电表读数为: {reading}") else: print("识别失败")

如何使用这段代码:

  1. 准备模板:从你的目标电表图片中,手动裁剪出清晰的0-9数字,每个数字存为一张单独的PNG图片(背景黑,数字白),统一放到./digit_templates文件夹下,并以数字命名(0.png, 1.png...)。
  2. 安装依赖:pip install opencv-python numpy
  3. 运行:将你的电表照片路径替换your_meter_photo.jpg,运行脚本。
  4. 调试:如果识别效果不好,打开visualize=True查看预处理和定位效果,然后有针对性地调整preprocess和find_and_sort_digits函数中的参数。

这个框架提供了一个坚实的起点。在实际应用中,你可能需要根据你的具体电表样式,微调预处理参数、轮廓筛选条件,尤其是精心制作匹配度高的模板。对于更复杂的环境,可以逐步引入前面提到的CNN方法或更复杂的后处理逻辑。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询