目录
[2.1 定期释放USB摄像头资源](#2.1 定期释放USB摄像头资源)
[2.2 命令行参数设置](#2.2 命令行参数设置)
[2.3 报警器控制指令](#2.3 报警器控制指令)
[2.4 控制报警器](#2.4 控制报警器)
[2.5 筛选列表中出现次数最多的元素](#2.5 筛选列表中出现次数最多的元素)
[2.6 筛选OCR识别结果中的型号](#2.6 筛选OCR识别结果中的型号)
[2.7 替换OCR识别中的易错字符](#2.7 替换OCR识别中的易错字符)
[2.8 隐藏警告符号+停止报警](#2.8 隐藏警告符号+停止报警)
[2.9 判断包装型号是否在产品型号中](#2.9 判断包装型号是否在产品型号中)
[2.10 计算字符面积-过滤过小的无效识别结果](#2.10 计算字符面积-过滤过小的无效识别结果)
[4.1 类的初始化](#4.1 类的初始化)
[4.2 相机开启/关闭方法](#4.2 相机开启/关闭方法)
[4.3 打开异常截图文件夹](#4.3 打开异常截图文件夹)
[4.4 复位按钮方法-界面按钮触发,清除识别结果](#4.4 复位按钮方法-界面按钮触发,清除识别结果)
[4.5 装饰器-控制OCR识别频率,优化性能](#4.5 装饰器-控制OCR识别频率,优化性能)
[4.6 画面刷新与OCR识别触发](#4.6 画面刷新与OCR识别触发)
[4.7 摄像头1的OCR识别与结果处理](#4.7 摄像头1的OCR识别与结果处理)
[4.8 摄像头2的OCR识别与结果处理](#4.8 摄像头2的OCR识别与结果处理)
[4.9 摄像头3的OCR识别与结果处理](#4.9 摄像头3的OCR识别与结果处理)
在现代工业生产中,产品型号与包装不匹配是常见质量问题。传统人工检测效率低、易疲劳。本文分享一套三摄像头实时 OCR 检测系统,可自动识别型号、异常报警、截图存档。
这个系统的核心功能是------用3个摄像头(2台海康工业相机+1台USB相机)实时采集画面,通过PaddleOCR识别产品和包装上的型号,自动比对是否匹配,出现异常时触发声光报警,还能自动保存异常截图,全程无需人工干预,典型的工业流水线防错场景应用。
系统功能
三路摄像头实时画面显示
PaddleOCR 英文文本识别
正则表达式精准筛选型号
字符纠错(0/O、S/5、I/1 等)
牛皮纸颜色触发识别
三路结果自动比对
声光异常报警
自动截图保存异常记录
PyQt5 图形化操作界面
技术栈
• Python
• OpenCV
• PaddleOCR
• PyQt5
• 串口通信
• 海康工业相机 SDK
• 多线程 & 定时器
系统架构
1.硬件配置
2路海康工业相机(千兆网口)
1路USB摄像头
2个声光报警器(通过串口控制)
2.软件架构
主界面模块-PyQt5
相机采集模块
OCR识别模块-pySerial
串口控制模块-pySerial
多线程管理模块
核心库安装
# 安装核心库(依次执行)
pip install opencv-python # 处理图像、摄像头采集
pip install paddlepaddle-gpu # PaddleOCR依赖(有GPU装这个,没有GPU装paddlepaddle)
pip install paddleocr # OCR文字识别核心库
pip install pyqt5 # 图形界面开发
pip install numpy # 数值计算(处理图像数组)
pip install pyserial # 串口通信(控制报警器)
pip install argparse # 命令行参数设置
pip install pillow # 辅助处理图像(可选)
需要准备文件
-
HKCamera_class.py:海康工业相机的控制类(从海康SDK中提取,文末有获取方式),放在代码同一文件夹下;
-
金寨窗口0.py:PyQt5的界面文件(用Qt Designer设计,无需自己设计,文末附简单说明)
项目整体逻辑
-
初始化:启动摄像头、报警器、PyQt5界面,设置相关参数(比如串口号、摄像头IP);
-
画面采集:3个摄像头实时采集画面,显示在PyQt5界面上;
-
OCR识别:每隔一定帧数,对采集到的画面进行文字识别,筛选出产品/包装型号;
-
型号比对:将工业相机识别的产品型号,与USB相机识别的包装型号进行比对;
-
异常处理:比对不一致时,触发声光报警,自动保存异常截图,5秒后自动停止报警。
记住这个逻辑,后续看代码就像"对号入座",知道每一段代码对应哪个步骤,就不会懵了。
完整代码逐段解析
模块1:导入所需库
python
import cv2 # 处理图像、摄像头采集
from collections import Counter # 统计列表中元素出现次数(筛选最可能的型号)
import os # 操作文件、保存截图
from numpy import ndarray # 处理图像数组(OpenCV返回的图像是ndarray类型)
import sys # 系统相关操作(比如添加库路径)
sys.path.append(r"D:\software\Pycharm\pyqt5库\MvImport") # 添加海康SDK路径(小白要改成自己的路径)
from HKCamera_class import HKCamera # 海康工业相机控制类(提前准备好的文件)
from PyQt5 import QtCore, QtGui, QtWidgets # PyQt5核心组件(界面开发)
from PyQt5.QtCore import * # PyQt5核心功能(定时器、线程等)
from PyQt5.QtGui import * # PyQt5图形相关(显示图像、文字等)
from PyQt5.QtWidgets import QFileDialog, QMainWindow # PyQt5窗口组件
from 金寨窗口0 import Ui_MainWindow # PyQt5界面文件(提前准备好的)
import logging # 日志输出(这里用来关闭日志,避免冗余信息)
import numpy as np # 数值计算(比如计算字符面积)
from paddleocr import PaddleOCR # PaddleOCR识别核心
import re # 正则表达式(筛选型号,过滤无效文字)
import serial # 串口通信(控制声光报警器)
import time # 时间相关(延时、获取当前时间,用于保存截图)
import argparse # 命令行参数设置(方便修改串口号、摄像头编号等)
import threading # 多线程(定期释放摄像头资源,避免卡顿)
sys.path.append(...):这里的路径要改成你自己电脑上海康SDK的路径,否则会报错"找不到HKCamera_class";
from 金寨窗口0 import Ui_MainWindow:如果你的界面文件名字不一样,要改成自己的文件名(比如你的界面文件叫"main_window.py",就改成from main_window import Ui_MainWindow);
其他库都是我们前期安装好的,导入失败就是没装对,重新执行安装命令即可。
模块2:辅助函数
为后续的摄像头控制、OCR识别、报警控制提供支持
2.1 定期释放USB摄像头资源
python
def release_capture3(cap): # 定期释放usb摄像头资源
while True:
# 每隔一段时间释放一次资源,这里设置为 30min(1800秒)
time.sleep(1800)
cap.release() # 释放摄像头资源
cap.open(opt.cap_numb3) # 重新打开摄像头
USB摄像头长时间运行会占用过多资源,导致画面卡顿、程序崩溃,这个函数用循环+延时,每30分钟释放一次资源,再重新打开,保证系统稳定运行。
2.2 命令行参数设置
python
parser = argparse.ArgumentParser()
# 第一个报警器的串口号(要改成自己的串口号,比如COM3、COM6)
parser.add_argument("--SERIAL_PORT1", type=str, default='COM5', help='第一个报警器的串口号')
# 第二、三个报警器的串口号(共用一个串口)
parser.add_argument("--SERIAL_PORT2", type=str, default='COM4', help='第二 三个报警器的串口号')
# 识别的置信度(0-1之间,越高识别越严格,避免误识别)
parser.add_argument("--confid_level", type=float, default=0.88, help='识别的置信度')
# 第三个摄像头编号(USB摄像头,一般是0或1,报错就换成0)
parser.add_argument("--cap_numb3", type=int, default=1, help='第三个摄像头编号')
# 获取画面帧数的延时(单位毫秒,越小画面越流畅,默认67ms≈15帧/秒)
parser.add_argument("--frame_delay", type=int, default=67, help='获取画面帧数的延时')
opt = parser.parse_args()
这里的参数可以直接在运行时修改,不用改代码!比如你的报警器串口号是COM3,运行时输入命令:python 你的代码文件名.py --SERIAL_PORT1 COM3,就能快速修改,非常方便。
2.3 报警器控制指令
python
######## 指令声明(声光报警器的控制指令,厂家提供,固定不变)
LIGHT_BUZZ1 = "0110001A000101CE18" # 闪光+声音1
LIGHT_BUZZ2 = "0110001A0001040E1B" # 闪光+声音2(备用)
LIGHT = "0110001A0001028E19" # 只闪光(不响铃)
BUZZ1 = "0110001A0001034FD9" # 只响铃(不闪光)
BUZZ2 = "0110001A000105CFDB" # 响铃2(备用)
BUZZ_CMD_CLOSE = "0110001A0001000FD8" # 关闭声音和闪光(报警结束后用)
logging.disable(logging.DEBUG) # 关闭日志的输出(避免屏幕出现大量冗余信息)
这些是声光报警器的"控制指令",不同厂家的指令可能不一样,这里是示例指令,如果用的是其他报警器,替换成厂家提供的指令即可,其余不用改。
2.4 控制报警器
python
def sendCmdToDevice(cmd, ser): # 控制警报器(cmd是控制指令,ser是串口对象)
cmdd = bytes.fromhex(cmd) # 将十六进制指令转换成字节(串口只能传输字节)
ser.write(cmdd) # 向串口发送指令,控制报警器
这个函数是"报警器的开关",后续只要调用这个函数,传入对应的指令(比如LIGHT)和串口对象,就能控制报警器闪光、响铃或关闭
2.5 筛选列表中出现次数最多的元素
python
def most_common_element(lst): # 提取列表中出现次数最多的字符(筛选标准型号)
# 使用Counter统计每个元素出现的次数(比如[123,123,456],统计后是{123:2, 456:1})
count = Counter(lst)
# 找到出现次数最多的元素(比如上面的123),作为标准型号
most_common_item = count.most_common(1)[0][0]
return most_common_item
OCR识别可能会有误差,比如一次识别出123,一次识别出123,一次识别出124,我们取出现次数最多的123作为"标准型号",减少误判。
2.6 筛选OCR识别结果中的型号
python
def process_string(input_string): # 在ocr的结果中筛选出对应型号(过滤无效文字)
aa=[]
# 用空格切分字符串(OCR识别结果可能包含多个文字,用空格分开)
parts = input_string.split()
# 正则表达式1: 包含数字和字母(比如A123、123B),长度2-10位(适配大部分型号)
pattern_alphanumeric = re.compile(r'^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z\d-]{2,10}$')
# 正则表达式2: 纯数字,至少4位(比如1234、56789)(适配纯数字型号)
pattern_at_least_two_digits = re.compile(r'^\d{4,7}$')
# 遍历每个切分后的部分,筛选出符合条件的型号
for part in parts:
if pattern_alphanumeric.match(part) or pattern_at_least_two_digits.match(part):
aa.append(part)
return aa
OCR识别会把画面中所有文字都识别出来(比如灰尘、污渍造成的乱码),这个函数用"正则表达式"筛选出"符合型号格式"的文字(数字+字母,或纯数字),过滤无效信息,如果需要调整型号格式,修改正则表达式即可(文末有补充)。
2.7 替换OCR识别中的易错字符
python
def set_bing(set_a): # 替换一些易错字符(OCR识别常出错的字符)
resu =set()
for j in set_a:
# 把容易识别错的字符替换成正确的(比如0和O、S和5、I和1)
jj = j.replace('0', 'O').replace('o', 'O').replace('s', '5').replace('S', '5').replace('I', '1').replace('L', '1').replace('v','V').replace('B','8').replace('p','P')
resu.add(jj)
return resu
OCR识别容易把"0"识别成"O"、"S"识别成"5",这个函数会自动替换这些易错字符,减少识别误差,我们可以根据自己的型号,添加更多易错字符的替换(比如把"Z"替换成"2")。
2.8 隐藏警告符号+停止报警
python
def hide_label_and_send_cmd(label,ser=None): # 隐藏警告符号,并停止报警
label.setVisible(False) # 隐藏PyQt5界面上的警告符号(⚠)
sendCmdToDevice(BUZZ_CMD_CLOSE, ser) # 发送"关闭"指令,停止报警器
2.9 判断包装型号是否在产品型号中
python
def In_which(text, hun):
a = 0
for i in hun:
if text in i: # 判断包装型号(text)是否在产品型号集合(hun)中
a += 1
return a # 返回1表示匹配,0表示不匹配
2.10 计算字符面积-过滤过小的无效识别结果
python
def are(i): # 计算OCR识别出的字符面积(过滤过小的乱码)
zs = i[0][0] # 字符左上角坐标
ys = i[0][1] # 字符右上角坐标
yx = i[0][2] # 字符右下角坐标
zx = i[0][3] # 字符左下角坐标
# 计算字符的宽度(取上下两条边的平均值,更准确)
width_A = np.sqrt(((zs[0] - ys[0]) ** 2) + ((zs[1] - ys[1]) ** 2))
width_B = np.sqrt(((zx[0] - yx[0]) ** 2) + ((zx[1] - yx[1]) ** 2))
# 计算字符的高度(取左右两条边的平均值,更准确)
height_A = np.sqrt(((zs[0] - zx[0]) ** 2) + ((zs[1] - zx[1]) ** 2))
height_B = np.sqrt(((ys[0] - yx[0]) ** 2) + ((ys[1] - yx[1]) ** 2))
# 取平均值作为最终的宽度和高度
width = (width_A + width_B) / 2
height = (height_A + height_B) / 2
# 计算面积(宽度×高度)
area = width * height
return area
画面中可能有很小的污渍,OCR会误识别成文字,这个函数计算识别出的字符面积,后续会过滤掉面积过小的结果,减少误判。
模块3:初始化OCR识别对象
python
# 初始化OCR识别对象(2个,分别用于不同场景)
# ocr:开启角度识别(use_angle_cls=True),用GPU加速,识别英文(lang='en')
ocr = PaddleOCR(use_angle_cls=True,use_gpu=True, lang='en')
# ocr2:关闭角度识别(速度更快),用GPU加速,识别英文(lang='en')
ocr2 = PaddleOCR(use_angle_cls=False, use_gpu=True, lang='en')
lang='en':因为我们识别的是产品型号(一般是英文+数字),所以设置为英文识别;如果需要识别中文,改成lang='ch';
use_gpu=True:如果没有GPU,改成use_gpu=False,否则会报错;
开启角度识别(use_angle_cls=True)会更准确,但速度稍慢,这里准备2个OCR对象,按需使用。
模块4:核心类
这部分是整个系统的"核心",所有功能(摄像头启动、画面显示、OCR识别、型号比对、报警)都在这个类里实现,我们拆成"初始化"和"核心方法"两部分讲解。
4.1 类的初始化
python
class PyQtMainEntry(QMainWindow, Ui_MainWindow):
def __init__(self):
super().__init__()
self.setupUi(self) # 初始化PyQt5界面(加载提前设计好的界面)
# 初始化串口(连接声光报警器)
self.ser1 = serial.Serial(opt.SERIAL_PORT1, 9600, timeout=2.5) # 第一个报警器串口
self.ser2 = serial.Serial(opt.SERIAL_PORT2, 9600, timeout=2.5) # 第二、三个报警器串口
# self.ser3 = serial.Serial(opt.SERIAL_PORT3, 9600, timeout=2.5) # 备用串口(注释掉了)
# 初始化界面上的警告符号(默认隐藏,异常时显示)
self.label_4.setVisible(False)
self.label_5.setVisible(False)
self.label_6.setVisible(False)
# 初始化存储识别结果的列表和集合(用于筛选标准型号)
self.list_zong1 = [] # 摄像头1的识别结果列表
self.list_zong2 = [] # 摄像头2的识别结果列表
self.list_zong3 = [] # 摄像头3的识别结果列表
self.set_zong1 = set() # 摄像头1的识别结果集合(去重)
self.set_zong2 = set() # 摄像头2的识别结果集合(去重)
self.set_zong3 = set() # 摄像头3的识别结果集合(去重)
self.set_12hun = set() # 摄像头1和2的识别结果合并集合
self.guo = [] # 摄像头3的临时识别结果列表
self.list1 = [] # 摄像头1的标准型号候选列表
self.list2 = [] # 摄像头2的标准型号候选列表
self.list3 = [] # 摄像头3的标准型号候选列表
self.daan1 = '' # 摄像头1的标准型号(最终确定的)
self.daan2 = '' # 摄像头2的标准型号(最终确定的)
# 初始化第一个工业相机(海康,IP:192.168.20.20)
self.camera1 = HKCamera(CameraIp='192.168.20.20')
# 设置相机参数(像素格式、自动增益、帧率)
self.camera1.set_Value(param_type="enum_value", node_name="PixelFormat", node_value='BayerGB8')
self.camera1.set_Value(param_type="enum_value", node_name="GainAuto", node_value='Continuous')
self.camera1.set_Value(param_type="float_value", node_name="AcquisitionFrameRate", node_value='15.0000')
self.camera1.start_camera() # 启动相机
# 初始化第二个工业相机(海康,IP:192.168.20.40)
self.camera2 = HKCamera(CameraIp='192.168.20.40')
# 设置相机参数(和第一个相机一致)
self.camera2.set_Value(param_type="enum_value", node_name="PixelFormat", node_value='BayerGB8')
self.camera2.set_Value(param_type="enum_value", node_name="GainAuto", node_value='Continuous')
self.camera2.set_Value(param_type="float_value", node_name="AcquisitionFrameRate", node_value='15.0000')
self.camera2.start_camera() # 启动相机
# 初始化第三个摄像头(USB相机,编号由命令行参数设置)
self.camera3 = cv2.VideoCapture(opt.cap_numb3)
# 启动后台线程,定期释放USB摄像头资源(避免卡顿)
release_thread2 = threading.Thread(target=release_capture3, args=(self.camera3,))
release_thread2.daemon = True # 设置为守护线程(主线程结束,子线程也结束)
release_thread2.start() # 启动线程
# 初始化定时器(控制画面刷新频率)
self.is_camera_opened = False # 相机是否开启(默认关闭)
self._timer = QtCore.QTimer(self) # 创建定时器对象
self._timer.timeout.connect(self._queryFrame) # 定时器触发时,调用_queryFrame方法(刷新画面)
self._timer.setInterval(opt.frame_delay) # 设置定时器间隔(由命令行参数设置)
self.frame_counter = 0 # 统计画面帧数(用于控制OCR识别频率)
工业相机IP(192.168.20.20、192.168.20.40):要改成你自己海康相机的IP,否则无法连接相机;如果没有工业相机,可注释掉工业相机相关代码,只保留USB相机(文末有补充);
串口初始化:如果串口号设置错误,会报错"无法打开串口",小白要先确认自己的报警器串口号(设备管理器中查看);
定时器:用于控制画面刷新频率,默认67ms刷新一次,也就是每秒刷新15次,画面流畅不卡顿。
4.2 相机开启/关闭方法
python
def openvideo(self):
self.is_camera_opened = not self.is_camera_opened # 切换相机状态(开启→关闭,关闭→开启)
if self.is_camera_opened:
self.pushButton_4.setText("关闭") # 按钮文字改成"关闭"
self._timer.start() # 启动定时器,开始刷新画面
else:
self.pushButton_4.setText("打开") # 按钮文字改成"打开"
self._timer.stop() # 停止定时器,停止刷新画面
# 关闭所有报警器
sendCmdToDevice(BUZZ_CMD_CLOSE, self.ser1)
sendCmdToDevice(BUZZ_CMD_CLOSE, self.ser2)
# 隐藏所有警告符号
self.label_4.setVisible(False)
self.label_5.setVisible(False)
self.label_6.setVisible(False)
这个方法绑定在PyQt5界面的"打开/关闭"按钮上,点击按钮就能开启或关闭相机,关闭时会自动停止报警、隐藏警告符号,非常人性化。
4.3 打开异常截图文件夹
python
def open_folder(self):
folder_path = r'D:\MVS\MVS\Development\Samples\Python\shiyan\baojing' # 截图保存路径
QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path)) # 打开文件夹
这里的路径要改成你自己想保存截图的路径,否则会报错"找不到文件夹",建议新建一个专门的文件夹,比如"D:\异常截图",然后修改路径。
4.4 复位按钮方法-界面按钮触发,清除识别结果
python
def clearSet1(self): # 复位按钮一(清除摄像头1的识别结果)
self.set_zong1.clear() # 清空集合
self.list1.clear() # 清空列表
sendCmdToDevice(BUZZ_CMD_CLOSE, self.ser1) # 停止报警器
self.label_4.setVisible(False) # 隐藏警告符号
def clearSet2(self): # 复位按钮二(清除摄像头2的识别结果)
self.set_zong2.clear() # 清空集合
self.list2.clear() # 清空列表
sendCmdToDevice(BUZZ_CMD_CLOSE, self.ser2) # 停止报警器
self.label_5.setVisible(False) # 隐藏警告符号
当识别结果有误时,点击复位按钮,就能清空之前的识别结果,重新开始识别,避免错误积累。
4.5 装饰器-控制OCR识别频率,优化性能
python
def execute_after_n_calls(n, w2set):
def decorator(func):
def wrapper(self, *args, **kwargs):
wrapper.count += 1 # 计数(每次调用函数,计数+1)
result = func(self, *args, **kwargs) # 执行OCR识别函数
if wrapper.count % n == 0: # 每调用n次,执行一次结果处理函数(w2set)
w2set(self)
return result
wrapper.count = 0 # 初始化计数为0
return wrapper
return decorator
这个是"装饰器",作用是"控制OCR识别频率"。比如给OCR识别函数加上@execute_after_n_calls(3, w2set1),就表示"每调用3次OCR识别函数,才处理一次识别结果",避免每帧都处理,减少CPU占用,提升系统流畅度。
4.6 画面刷新与OCR识别触发
python
@QtCore.pyqtSlot()
def _queryFrame(self):
try:
# 检查USB相机是否能采集到画面
if not self.camera3.grab():
print("No frame grabbed.") # 打印错误信息
self.camera3.release() # 释放相机资源
self.close() # 关闭窗口
else:
# 从3个相机中获取画面
self.frame1: ndarray = self.camera1.get_image() # 工业相机1画面
self.frame2: ndarray = self.camera2.get_image() # 工业相机2画面
ret3, self.frame3 = self.camera3.read() # USB相机3画面
# 检查USB相机是否获取到画面
if not ret3:
print("No frame retrieved.") # 打印错误信息
self.camera3.release() # 释放相机资源
self.close() # 关闭窗口
else:
# 调整画面尺寸(640×480,适配PyQt5界面)
self.frame11 = cv2.resize(self.frame1, (640, 480))
self.frame22 = cv2.resize(self.frame2, (640, 480))
self.frame33 = cv2.resize(self.frame3, (640, 480))
# 将OpenCV画面(BGR格式)转换成PyQt5能显示的格式(RGB格式)
qimage = cv2.cvtColor(self.frame11.copy(), cv2.COLOR_BGR2RGB)
qimage = QtGui.QImage(qimage.data, qimage.shape[1], qimage.shape[0], QtGui.QImage.Format_RGB888)
pixmap = QtGui.QPixmap.fromImage(qimage) # 转换成Pixmap格式
qimage2 = cv2.cvtColor(self.frame22.copy(), cv2.COLOR_BGR2RGB)
qimage2 = QtGui.QImage(qimage2.data, qimage2.shape[1], qimage2.shape[0], QtGui.QImage.Format_RGB888)
pixmap2 = QtGui.QPixmap.fromImage(qimage2)
qimage3 = cv2.cvtColor(self.frame33.copy(), cv2.COLOR_BGR2RGB)
qimage3 = QtGui.QImage(qimage3.data, qimage3.shape[1], qimage3.shape[0], QtGui.QImage.Format_RGB888)
pixmap3 = QtGui.QPixmap.fromImage(qimage3)
# 将画面显示在PyQt5界面的标签上
self.label.setPixmap(pixmap)
self.label_3.setPixmap(pixmap2)
self.label_2.setPixmap(pixmap3)
# 统计画面帧数,控制OCR识别频率
self.frame_counter += 1
if self.frame_counter % 5 == 0: # 每5帧,执行一次摄像头1的OCR识别
self._performOCR1()
if self.frame_counter % 3 == 0: # 每3帧,执行一次摄像头2的OCR识别
self._performOCR2()
if self.frame_counter % 3 == 0: # 每3帧,执行一次摄像头3的OCR识别
self._performOCR3()
except:
pass # 捕获所有错误,避免程序崩溃
这个方法是"画面刷新的核心",定时器每触发一次,就执行一次这个方法,完成3件事:
从3个相机获取画面,检查画面是否正常;
调整画面尺寸,转换成PyQt5能显示的格式,显示在界面上;
根据帧数,触发对应的OCR识别(不同摄像头识别频率不同,优化性能)。
4.7 摄像头1的OCR识别与结果处理
python
@execute_after_n_calls(3, w2set1) # 每3次识别,执行一次w2set1(结果处理)
def _performOCR1(self):
# 对摄像头1的画面进行OCR识别(用ocr2,关闭角度识别,速度更快)
result = ocr2.ocr(self.frame1, cls=False)
# 检查识别结果是否有效(不为空,且没有None)
if result and not None in result:
try:
for i in result[0]: # 遍历识别出的每一个字符
mianji = are(i) # 计算字符面积
ma = process_string(i[1][0]) # 筛选出符合条件的型号
# 过滤条件:置信度>0.92(识别准确率高)、有有效型号、字符位置在320-1060之间、面积>1000(过滤小乱码)
if i[1][1] > 0.92 and ma and 320 < i[0][0][0]< 1060 and mianji > 1000:
self.list_zong1.extend(ma) # 将有效型号添加到列表中
except:
pass
def w2set1(self):
if self.list_zong1: # 如果列表中有识别结果
try:
# 筛选出出现次数>1的型号(减少误识别)
for i in self.list_zong1:
if self.list_zong1.count(i) > 1:
self.set_zong1.add(i)
set_len1 = set_bing(self.set_zong1) # 替换易错字符,得到当前检测结果
self.list1.extend(list(set_len1)) # 添加到标准型号候选列表
self.daan1 = most_common_element(self.list1) # 筛选出出现次数最多的,作为标准型号
# 每16帧(约10秒)清空一次候选列表,避免结果积累
if self.frame_counter % 16 == 0:
self.list1.clear()
# 将识别结果显示在PyQt5界面的文本框中
text = ','.join(set_len1)
self.lineEdit.setText(text)
# 型号比对:如果没有标准型号,或没有当前检测结果,不处理
if (not self.daan1) or (not set_len1):
pass
# 如果当前检测结果只有一个,且和标准型号一致,正常运行
elif len(set_len1) == 1 and (list(set_len1)[0] == self.daan1):
pass
# 否则,判定为异常,触发报警
else:
print('一')
print(set_len1, self.daan1) # 打印异常信息(调试用)
# 在画面上添加异常文字(红色,醒目)
tu1 = cv2.putText(self.frame1.copy(), text, (100, 200), cv2.FONT_HERSHEY_SIMPLEX, 2, (0, 0, 255), 5, cv2.LINE_AA)
# 保存异常截图(以当前时间命名,方便追溯)
nam = time.strftime("%Y_%m_%d_%H_%M", time.localtime())
path = os.path.join(r'D:\MVS\MVS\Development\Samples\Python\shiyan\baojing\镜头一', f'{nam}-{self.daan1}.jpg')
cv2.imencode('.jpg', tu1)[1].tofile(path)
self.label_4.setVisible(True) # 显示警告符号
sendCmdToDevice(LIGHT, self.ser1) # 触发报警器(只闪光)
# 5秒后,隐藏警告符号,停止报警
QtCore.QTimer.singleShot(5000, lambda: hide_label_and_send_cmd(self.label_4,self.ser1))
self.frame_counter = 0 # 重置帧数计数器
except Exception as e:
print(e) # 打印错误信息(调试用)
# 清空当前识别结果,准备下一次识别
self.list_zong1.clear()
self.set_zong1.clear()
这两个方法配合使用,_performOCR1负责"识别型号",w2set1负责"处理识别结果、判断是否异常",逻辑和摄像头1完全一致,重点看过滤条件和异常处理流程即可
4.8 摄像头2的OCR识别与结果处理
python
@execute_after_n_calls(3, w2set2) # 每3次识别,执行一次w2set2(结果处理)
def _performOCR2(self):
# 执行OCR操作(和摄像头1一致,只是过滤条件不同)
result2 = ocr2.ocr(self.frame2, cls=False)
if result2 and not None in result2:
try:
for i in result2[0]:
mianji=are(i)
ma=process_string(i[1][0])
# 过滤条件:置信度>0.84、有有效型号、面积>1600、位置在350-1060之间
if i[1][1] > 0.84 and ma and mianji>1600 and (350<i[0][0][0]<1060):
self.list_zong2.extend(ma)
except:
pass
elif None in result2: # 如果识别结果为空,清空标准型号
self.daan2=''
self.set_12hun = set([self.daan1, self.daan2])
print('1:', self.daan1)
print('2:', self.daan2)
print('合:', self.set_12hun)
def w2set2(self):
if self.list_zong2:
try:
# 和摄像头1的w2set1逻辑一致,筛选出现次数>1的型号
for i in self.list_zong2:
if self.list_zong2.count(i) > 1:
self.set_zong2.add(i)
set_len2 = set_bing(self.set_zong2)
self.list2.extend(list(set_len2))
self.daan2 = most_common_element(self.list2)
# 每25帧(约10秒)清空一次候选列表
if self.frame_counter % 25 == 0:
self.list2.clear()
# 将结果显示在界面文本框中
text2 = ','.join(set_len2)
self.lineEdit_2.setText(text2)
# 型号比对,逻辑和摄像头1一致
if (not self.daan2) or (not set_len2):
pass
elif len(set_len2)==1 and (list(set_len2)[0] == self.daan2):
pass
else:
print('二')
print(set_len2, self.daan2)
# 在画面上添加异常文字,保存截图
tu2 = cv2.putText(self.frame2.copy(), text2, (100, 200), cv2.FONT_HERSHEY_SIMPLEX, 3, (0, 0, 255), 6, cv2.LINE_AA)
nam2 = time.strftime("%Y_%m_%d_%H_%M", time.localtime())
path = os.path.join(r'D:\MVS\MVS\Development\Samples\Python\shiyan\baojing\镜头二', f'{nam2}-{self.daan2}.jpg')
cv2.imencode('.jpg', tu2)[1].tofile(path)
self.label_5.setVisible(True) # 显示警告符号
sendCmdToDevice(LIGHT, self.ser2) # 触发报警器
QtCore.QTimer.singleShot(5000, lambda: hide_label_and_send_cmd(self.label_5, self.ser2))
self.frame_counter = 0 # 重置帧数
except Exception as e:
print(e)
# 清空识别结果
self.list_zong2.clear()
self.set_zong2.clear()
# 合并摄像头1和2的标准型号,用于和摄像头3的包装型号比对
self.set_12hun=set([self.daan1,self.daan2])
print('1:', self.daan1)
print('2:', self.daan2)
print('合:', self.set_12hun)
摄像头2的过滤条件(置信度、面积、位置)和摄像头1不同,是因为两个工业相机拍摄的角度、距离不同,可以根据自己的实际情况调整这些参数。
4.9 摄像头3的OCR识别与结果处理
python
@execute_after_n_calls(3, w2set3) # 每3次识别,执行一次w2set3(结果处理)
def _performOCR3(self):
global text3
try:
# 新增:牛皮纸颜色检测(只识别牛皮纸包装,过滤其他无关画面)
# 将图像从BGR格式转换成HSV格式(HSV格式更适合颜色检测)
hsv_image = cv2.cvtColor(self.frame3, cv2.COLOR_BGR2HSV)
# 定义牛皮纸颜色的范围(HSV值,可调整)
lower_brown = np.array([10, 30, 30]) # 最小颜色值
upper_brown = np.array([30, 255, 255]) # 最大颜色值
# 筛选出牛皮纸颜色的区域(mask是掩码,白色区域是牛皮纸)
mask = cv2.inRange(hsv_image, lower_brown, upper_brown)
# 计算牛皮纸颜色的占比
white_pixels = np.sum(mask == 255) # 白色像素数量(牛皮纸区域)
total_pixels = mask.shape[0] * mask.shape[1] # 图像总像素数
color_percentage = (white_pixels / total_pixels) * 100 # 牛皮纸占比
# 只有牛皮纸占比≥20%,才执行OCR识别(避免无关画面误识别)
if color_percentage >= 20:
result2 = ocr2.ocr(self.frame3, cls=False)
if result2 and not None in result2:
for j in result2[0]:
# 过滤条件:置信度>0.93、有有效型号
if j[1][1] > 0.93 and process_string(j[1][0]):
self.list_zong3.append(j)
else:
text3 = '' # 识别结果为空,置空包装型号
except:
text3 = '' # 出现错误,置空包装型号
def w2set3(self):
maxx = 0 # 用于筛选面积最大的字符(最清晰的型号)
if self.list_zong3:
try:
# 遍历识别结果,筛选出现次数>1的型号
for i in self.list_zong3:
self.guo.append(i[1][0])
for ii in self.guo:
if self.guo.count(ii) > 1:
self.set_zong3.add(ii)
# 筛选出面积最大的型号(最清晰,误差最小)
for j in self.list_zong3:
if (j[1][0] in self.set_zong3) and (are(j) > maxx):
aa = j
maxx = are(j)
# 替换易错字符,得到包装型号
text3 = aa[1][0].replace('0', 'O').replace('o', 'O').replace('S', '5').replace('I', '1').replace('L','1').replace('v','V').replace('B','8').replace('p','P')
self.lineEdit_3.setText(text3) # 显示在界面文本框中
# 型号比对:包装型号与产品型号(摄像头1+2)比对
if (not text3) or self.set_12hun==set() or self.set_12hun=={''}:
pass # 有一个为空,不处理
elif self.set_12hun and In_which(text3,self.set_12hun):
pass # 包装型号在产品型号中,匹配正常
else:
# 不匹配,判定为异常,触发报警
print('三')
print(text3)
print(self.set_12hun)
# 添加异常文字,保存截图
tu3 = cv2.putText(self.frame3.copy(), text3, (100, 200), cv2.FONT_HERSHEY_SIMPLEX, 2, (0, 0, 255), 5, cv2.LINE_AA)
nam3 = time.strftime("%Y_%m_%d_%H_%M", time.localtime())
path = os.path.join(r'D:\MVS\MVS\Development\Samples\Python\shiyan\baojing\镜头三', f'{nam3}-{self.set_12hun}.jpg')
cv2.imencode('.jpg', tu3)[1].tofile(path)
self.set_12hun.clear() # 清空产品型号集合
self.label_6.setVisible(True) # 显示警告符号
sendCmdToDevice(LIGHT, self.ser2) # 触发报警器
QtCore.QTimer.singleShot(5000, lambda: hide_label_and_send_cmd(self.label_6, self.ser2))
except:
pass
# 清空识别结果
self.guo.clear()
self.list_zong3.clear()
self.set_zong3.clear()
包装型号识别,新增颜色过滤
摄像头3是"包装型号识别",和前两个摄像头最大的区别是"新增了牛皮纸颜色检测"------只识别牛皮纸包装的画面,避免其他无关画面(比如桌面、灰尘)误识别,这是工业场景中非常实用的优化,可以根据自己的包装颜色,调整HSV值。
模块5:程序入口-启动程序
python
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv) # 创建PyQt5应用程序对象
window = PyQtMainEntry() # 创建主窗口对象
window.show() # 显示主窗口
sys.exit(app.exec_()) # 启动应用程序,进入消息循环
其实工业视觉OCR检测项目,并没有想象中那么高深复杂,核心逻辑就4步:画面采集→文字识别→结果比对→异常处理。只要将复杂流程拆解开来,逐模块学习、逐段调试,就能轻松上手、顺利复刻。
本文从前期环境搭建、项目逻辑梳理,到代码逐段解析、实战避坑指南,再到补充资源、进阶方向,只需跟着步骤操作,就能复刻出属于自己的工业视觉型号检测系统。这里重点提醒:遇到报错不要慌,优先排查路径、版本、硬件连接这三大核心问题;代码无需死记硬背,理解每个模块的核心作用,才能举一反三,适配不同的工业检测场景。