Python+OpenCV实战:LED数码管电表读数自动识别技术详解

📅 发布时间:2026/9/3 2:17:29
Python+OpenCV实战:LED数码管电表读数自动识别技术详解 简介本资源面向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。这些方案对于文档、车牌等场景确实强大但在我们这种特定场景下可能会遇到几个致命问题误识别率高LED数字的“1”可能被识别成“I”或“l”断开的“8”可能被识别成“0”或“6”通用模型没有针对这种字体进行优化。定位困难电表图像可能包含表盘、铭牌、按钮等多种信息通用OCR需要先进行文本检测在复杂背景下可能无法准确框出我们关心的读数区域。成本与依赖使用云API会产生费用并且需要网络。对于本地化、离线部署的应用场景比如嵌入式设备不友好。定制化差当电表型号更换LED显示样式颜色、粗细、间距发生变化时通用模型难以快速适配。因此我们决定采用传统图像处理 定制化模板匹配/特征识别的路线。这条路线的优势在于原理透明可控性强每一步处理滤波、二值化、轮廓查找的结果都肉眼可见出了问题容易调试。不依赖网络和大型模型完全本地运行速度快资源消耗低。高度定制化可以根据具体的电表样式调整每一个处理参数达到最优识别效果。我们的核心处理流程可以概括为四个阶段图像预处理把拍到的彩色/灰度图处理成干净、高对比度的二值图黑白图让LED数字区域“凸显”出来。数字区域定位与分割找到图像中所有可能是数字的区域并把它们一个个“抠出来”。数字识别对每一个抠出来的数字子图判断它是0-9中的哪一个。结果输出将识别出的单个数字按顺序组合成最终读数。下面我们就沿着这个流程一步步拆解其中的技术细节和实操代码。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(clipLimit2.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数字通常具有以下特征宽高比单个数字的轮廓 bounding rect 的宽高比有一定范围比如在0.2到1.2之间“1”比较瘦长“0”比较宽。面积面积不能太小排除噪声点也不能太大排除整个表盘。占空比轮廓面积与其外接矩形面积的比值即填充率数字的填充率不会太低也不会是实心矩形。位置多个数字通常在同一水平线上且间距大致相等。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, keylambda 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:yh, x:xw] # 标准化统一缩放到固定大小比如28x28类似MNIST数据集 # 首先计算需要添加的边框使图像居中 if h w: # 高度大于宽度在左右两侧加边框 padding (h - w) // 2 roi cv2.copyMakeBorder(roi, 0, 0, padding, padding, cv2.BORDER_CONSTANT, value0) else: # 宽度大于高度在上下两侧加边框 padding (w - h) // 2 roi cv2.copyMakeBorder(roi, padding, padding, 0, 0, cv2.BORDER_CONSTANT, value0) # 缩放到目标尺寸 target_size (28, 28) roi_resized cv2.resize(roi, target_size, interpolationcv2.INTER_AREA) # 可以添加一个简单的二值化确保是纯黑白因为resize可能产生灰度 _, roi_final cv2.threshold(roi_resized, 127, 255, cv2.THRESH_BINARY) digits.append(roi_final) # 可视化在原图上画出矩形框 cv2.rectangle(image, (x, y), (xw, yh), (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_size0.2) # 4. 训练SVM分类器 # clf svm.SVC(kernellinear, C1.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_classes10): model keras.Sequential([ layers.Conv2D(32, (3, 3), activationrelu, input_shapeinput_shape), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activationrelu), layers.MaxPooling2D((2, 2)), layers.Flatten(), layers.Dense(128, activationrelu), layers.Dropout(0.5), layers.Dense(num_classes, activationsoftmax) ]) model.compile(optimizeradam, losssparse_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, epochs10, 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 方法选择与实战建议对于刚入门或者想快速验证的开发者我推荐模板匹配法。它的实现最简单只要你能制作出与目标电表字体高度一致的模板识别率可以非常高。而且整个过程透明调试方便。当你需要处理多种不同样式的电表或者模板匹配效果不稳定时可以考虑SVMHOG的方法。它比模板匹配更鲁棒又比CNN简单。如果你的项目对准确率要求极高或者需要部署在多种复杂环境下不同品牌、不同型号的电表那么投入时间收集数据、训练一个轻量级CNN模型是值得的。训练好的模型可以保存为.h5或.tflite文件在Python甚至移动端、嵌入式设备上都能方便地调用。6. 避坑指南与性能优化在实际部署中你会遇到很多在理想测试图片中遇不到的问题。下面分享几个我踩过的坑和解决方案。6.1 数字粘连与断裂的处理问题预处理时闭运算过度或者原始图像中数字间距太小导致两个数字的轮廓被连成一个。排查在画出轮廓矩形后观察矩形的宽度。如果某个矩形的宽度明显是其他数字的2倍左右很可能包含了两个数字。解决调整预处理减小闭运算的核大小或者尝试不使用闭运算改用其他方法如中值滤波去除噪声。轮廓后处理对过宽的轮廓计算其X方向的投影直方图。在两个数字之间投影值会有一个明显的低谷。可以尝试在这个低谷处进行分割。使用更精细的检测方法可以考虑使用连通组件分析并结合数字的预期宽高比进行筛选和分割。6.2 复杂背景与干扰项的过滤问题电表玻璃反光、金属边框、螺丝、其他文字如“kWh”被误识别为数字。排查打印出所有被筛选出的轮廓及其属性面积、宽高比、位置。解决位置先验如果知道数字大致出现在图像的某个区域如中间偏右可以设置一个ROI感兴趣区域只在这个区域内查找轮廓。多级筛选除了面积和宽高比可以加入轮廓的层次关系hierarchy筛选排除被嵌套的轮廓可能是数字内部空洞也可能是其他结构。还可以利用数字的排列规律多个有效数字的Y坐标顶部应该大致相同高度也相近。可以对筛选出的轮廓进行聚类只保留最大的、符合水平排列规律的那一组。6.3 识别置信度与结果校验问题某个数字识别错了导致整个读数错误。解决设置置信度阈值无论是模板匹配的相似度得分还是CNN预测的softmax概率都设定一个阈值比如0.7。低于阈值的识别结果标记为“不确定”可以触发报警如记录日志、要求人工复核而不是直接使用。业务逻辑校验电表读数通常有范围。比如新装电表读数不可能为999999两次抄表间的读数差不会离谱地大。可以在输出最终结果前加入这类简单的业务规则校验。多帧投票对于视频流或连续拍摄的多张图片可以对同一块电表识别多次然后对每一位数字进行投票取出现次数最多的结果可以有效抑制单次识别的偶然错误。6.4 性能优化技巧如果需要在树莓派等资源受限的设备上运行可以采取以下优化降低图像分辨率在保证数字清晰可辨的前提下将输入图像缩放至较小的尺寸如640x480能大幅减少处理时间。固定ROI如果摄像头和电表位置固定可以预先标定好数字出现的像素区域后续只处理这个区域避免全图搜索。选择轻量级识别方法模板匹配的速度远快于CNN。在满足准确率要求的前提下优先使用。使用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), interpolationcv2.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(clipLimit2.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(keylambda b: b[0]) return digit_boxes def extract_digit_image(self, binary_image, box): 从二值图中提取单个数字图像并标准化 x, y, w, h, _ box roi binary_image[y:yh, x:xw] # 添加边框使其成为正方形 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, value0) else: roi cv2.copyMakeBorder(roi, pad1, pad2, 0, 0, cv2.BORDER_CONSTANT, value0) # 缩放到28x28 roi cv2.resize(roi, (28, 28), interpolationcv2.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, visualizeFalse): 主函数读取电表图像并返回识别结果 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), (xw, yh), (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, visualizeTrue) if reading: print(f识别到的电表读数为: {reading}) else: print(识别失败)如何使用这段代码准备模板从你的目标电表图片中手动裁剪出清晰的0-9数字每个数字存为一张单独的PNG图片背景黑数字白统一放到./digit_templates文件夹下并以数字命名0.png, 1.png...。安装依赖pip install opencv-python numpy运行将你的电表照片路径替换your_meter_photo.jpg运行脚本。调试如果识别效果不好打开visualizeTrue查看预处理和定位效果然后有针对性地调整preprocess和find_and_sort_digits函数中的参数。这个框架提供了一个坚实的起点。在实际应用中你可能需要根据你的具体电表样式微调预处理参数、轮廓筛选条件尤其是精心制作匹配度高的模板。对于更复杂的环境可以逐步引入前面提到的CNN方法或更复杂的后处理逻辑。本文还有配套的精品资源点击获取