目录引言技术栈系统架构1. 硬件配置2. 软件架构核心功能实现1. 相机初始化与管理2. 多路视频显示3. OCR识别优化字符筛选策略易错字符替换区域面积筛选4. 报警逻辑实现5. 多线程资源管理界面设计布局特点样式设置性能优化1. OCR调用频率控制2. 日志管理3. GPU加速遇到的问题及解决方案1. USB摄像头资源泄露2. OCR误识别3. 多线程显示卡顿使用效果总代码1.界面代码2.执行代码引言在工业自动化领域视觉识别与实时报警系统有着广泛的应用场景。本文将分享一个基于PyQt5、PaddleOCR和海康工业相机的多路视觉识别报警系统的开发经验。该系统能够同时监控三个摄像头对识别到的特定产品型号进行实时报警。技术栈GUI框架PyQt5视觉识别PaddleOCR (GPU加速)工业相机海康工业相机 (通过SDK控制)USB相机OpenCV报警控制串口通信多线程Python threading系统架构1. 硬件配置2路海康工业相机千兆网口1路USB摄像头2个声光报警器通过串口控制2. 软件架构├── 主界面模块 (PyQt5) ├── 相机采集模块 (海康SDK OpenCV) ├── OCR识别模块 (PaddleOCR) ├── 串口控制模块 (pySerial) └── 多线程管理模块核心功能实现1. 相机初始化与管理# 海康工业相机初始化 self.camera1 HKCamera(CameraIp192.168.20.56) self.camera1.set_Value(param_typeenum_value, node_namePixelFormat, node_valueBayerGB8) self.camera1.set_Value(param_typefloat_value, node_nameAcquisitionFrameRate, node_value15.0000)关键点工业相机需要设置像素格式和帧率采用连续自动增益确保图像质量USB相机需要定期释放资源避免内存泄漏2. 多路视频显示通过定时器实现三路视频的实时显示self._timer.timeout.connect(self._queryFrame) self._timer.setInterval(67) # 约15fps在_queryFrame方法中同时获取三路图像并显示在对应的Label控件上。3. OCR识别优化字符筛选策略def process_string(input_string): # 正则表达式1: 包含数字和字母的组合 pattern_alphanumeric re.compile(r^(?.*[a-zA-Z])(?.*\d)[a-zA-Z\d-]{2,10}$) # 正则表达式2: 4-7位纯数字 pattern_at_least_two_digits re.compile(r^\d{4,7}$)易错字符替换def set_bing(set_a): # 替换易混淆字符 jj j.replace(0, O).replace(o, O).replace(s, 5) .replace(S, 5).replace(I, 1).replace(L, 1)区域面积筛选通过计算识别区域的面积过滤掉过小或过大的无效识别def are(i): # 计算四边形面积 width (width_A width_B) / 2 height (height_A height_B) / 2 area width * height return area4. 报警逻辑实现def sendCmdToDevice(cmd, ser): cmdd bytes.fromhex(cmd) ser.write(cmdd) ​ # 不同报警模式 LIGHT_BUZZ1 0110001A000101CE18 # 闪光声音1 LIGHT 0110001A0001028E19 # 仅闪光 BUZZ1 0110001A0001034FD9 # 仅声音1 BUZZ_CMD_CLOSE 0110001A0001000FD8 # 关闭5. 多线程资源管理针对USB摄像头设计定期释放机制def release_capture3(cap): while True: time.sleep(1800) # 30分钟释放一次 cap.release() cap.open(opt.cap_numb3)界面设计布局特点三路视频并排显示三个独立的识别结果显示框三个声光报警指示灯复位按钮和报警信息查询样式设置self.label_4.setGeometry(QtCore.QRect(430, 690, 61, 61)) self.label_4.setText(htmlhead/bodyp align\center\span style\ font-size:18pt;\⚠/span/p/body/html)性能优化1. OCR调用频率控制if self.frame_counter % 5 0: self._performOCR1() if self.frame_counter % 3 0: self._performOCR2() self._performOCR3()2. 日志管理logging.disable(logging.DEBUG) # 关闭不必要的日志输出3. GPU加速ocr PaddleOCR(use_angle_clsTrue, use_gpuTrue, langen)遇到的问题及解决方案1. USB摄像头资源泄露问题长时间运行后USB摄像头无法打开解决使用守护线程定期释放和重连2. OCR误识别问题字符识别错误率高解决多重筛选 易错字符替换 面积过滤3. 多线程显示卡顿问题三路视频同时显示导致界面卡顿解决降低部分摄像头的OCR频率优化帧率设置使用效果系统在实际工业环境中运行稳定能够准确识别产品型号并触发相应的声光报警。三个摄像头可同时工作互不干扰满足生产线的实时监控需求总代码1.界面代码# -*- coding: utf-8 -*- # Form implementation generated from reading ui file 金寨窗口0.ui # # Created by: PyQt5 UI code generator 5.15.9 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. from PyQt5 import QtCore, QtGui, QtWidgets class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName(MainWindow) MainWindow.resize(1920, 1080) sizePolicy QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(MainWindow.sizePolicy().hasHeightForWidth()) MainWindow.setSizePolicy(sizePolicy) self.centralwidget QtWidgets.QWidget(MainWindow) self.centralwidget.setObjectName(centralwidget) self.label QtWidgets.QLabel(self.centralwidget) self.label.setGeometry(QtCore.QRect(10, 10, 631, 421)) self.label.setObjectName(label) self.label_2 QtWidgets.QLabel(self.centralwidget) self.label_2.setGeometry(QtCore.QRect(1300, 10, 611, 421)) self.label_2.setObjectName(label_2) self.label_3 QtWidgets.QLabel(self.centralwidget) self.label_3.setGeometry(QtCore.QRect(660, 10, 621, 421)) self.label_3.setObjectName(label_3) self.pushButton QtWidgets.QPushButton(self.centralwidget) self.pushButton.setGeometry(QtCore.QRect(230, 670, 141, 61)) self.pushButton.setStyleSheet(font: 20pt \Arial\;) self.pushButton.setObjectName(pushButton) self.pushButton_2 QtWidgets.QPushButton(self.centralwidget) self.pushButton_2.setGeometry(QtCore.QRect(870, 670, 141, 61)) self.pushButton_2.setStyleSheet(font: 20pt \Arial\;) self.pushButton_2.setObjectName(pushButton_2) self.lineEdit QtWidgets.QLineEdit(self.centralwidget) self.lineEdit.setGeometry(QtCore.QRect(200, 540, 211, 61)) self.lineEdit.setStyleSheet(font: 63 24pt \Segoe UI Semibold\;) self.lineEdit.setText() self.lineEdit.setObjectName(lineEdit) self.pushButton_4 QtWidgets.QPushButton(self.centralwidget) self.pushButton_4.setGeometry(QtCore.QRect(870, 850, 221, 111)) self.pushButton_4.setStyleSheet(font: 20pt \Arial\;) self.pushButton_4.setObjectName(pushButton_4) self.lineEdit_2 QtWidgets.QLineEdit(self.centralwidget) self.lineEdit_2.setGeometry(QtCore.QRect(850, 540, 211, 61)) self.lineEdit_2.setStyleSheet(font: 63 24pt \Segoe UI Semibold\;) self.lineEdit_2.setText() self.lineEdit_2.setObjectName(lineEdit_2) self.lineEdit_3 QtWidgets.QLineEdit(self.centralwidget) self.lineEdit_3.setGeometry(QtCore.QRect(1530, 540, 211, 61)) self.lineEdit_3.setStyleSheet(font: 63 24pt \Segoe UI Semibold\;) self.lineEdit_3.setText() self.lineEdit_3.setObjectName(lineEdit_3) self.line QtWidgets.QFrame(self.centralwidget) self.line.setGeometry(QtCore.QRect(-10, 780, 1921, 20)) sizePolicy QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.line.sizePolicy().hasHeightForWidth()) self.line.setSizePolicy(sizePolicy) self.line.setFrameShape(QtWidgets.QFrame.HLine) self.line.setFrameShadow(QtWidgets.QFrame.Sunken) self.line.setObjectName(line) self.line_2 QtWidgets.QFrame(self.centralwidget) self.line_2.setGeometry(QtCore.QRect(640, 0, 20, 791)) self.line_2.setFrameShape(QtWidgets.QFrame.VLine) self.line_2.setFrameShadow(QtWidgets.QFrame.Sunken) self.line_2.setObjectName(line_2) self.line_3 QtWidgets.QFrame(self.centralwidget) self.line_3.setGeometry(QtCore.QRect(1280, 0, 20, 791)) self.line_3.setFrameShape(QtWidgets.QFrame.VLine) self.line_3.setFrameShadow(QtWidgets.QFrame.Sunken) self.line_3.setObjectName(line_3) self.line_4 QtWidgets.QFrame(self.centralwidget) self.line_4.setGeometry(QtCore.QRect(-10, 430, 1941, 20)) self.line_4.setFrameShape(QtWidgets.QFrame.HLine) self.line_4.setFrameShadow(QtWidgets.QFrame.Sunken) self.line_4.setObjectName(line_4) self.label_4 QtWidgets.QLabel(self.centralwidget) self.label_4.setGeometry(QtCore.QRect(430, 690, 61, 61)) self.label_4.setObjectName(label_4) self.label_5 QtWidgets.QLabel(self.centralwidget) self.label_5.setGeometry(QtCore.QRect(1080, 680, 61, 61)) self.label_5.setObjectName(label_5) self.label_6 QtWidgets.QLabel(self.centralwidget) self.label_6.setGeometry(QtCore.QRect(1610, 670, 71, 61)) self.label_6.setObjectName(label_6) self.pushButton_3 QtWidgets.QPushButton(self.centralwidget) self.pushButton_3.setGeometry(QtCore.QRect(1580, 880, 201, 91)) self.pushButton_3.setStyleSheet(font: 20pt \Arial\;) self.pushButton_3.setObjectName(pushButton_3) MainWindow.setCentralWidget(self.centralwidget) self.menubar QtWidgets.QMenuBar(MainWindow) self.menubar.setGeometry(QtCore.QRect(0, 0, 1920, 22)) self.menubar.setObjectName(menubar) MainWindow.setMenuBar(self.menubar) self.statusbar QtWidgets.QStatusBar(MainWindow) self.statusbar.setObjectName(statusbar) MainWindow.setStatusBar(self.statusbar) self.retranslateUi(MainWindow) self.pushButton_4.clicked.connect(MainWindow.openvideo) # type: ignore self.pushButton.clicked.connect(self.lineEdit.clear) # type: ignore self.pushButton.clicked.connect(MainWindow.clearSet1) # type: ignore self.pushButton_2.clicked.connect(self.lineEdit_2.clear) # type: ignore self.pushButton_2.clicked.connect(MainWindow.clearSet2) # type: ignore self.pushButton_3.clicked.connect(MainWindow.open_folder) # type: ignore QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): _translate QtCore.QCoreApplication.translate MainWindow.setWindowTitle(_translate(MainWindow, MainWindow)) self.label.setText(_translate(MainWindow, htmlhead/bodyp align\center\span style\ font-size:18pt;\视频1/span/p/body/html)) self.label_2.setText(_translate(MainWindow, htmlhead/bodyp align\center\span style\ font-size:18pt;\视频3/span/p/body/html)) self.label_3.setText(_translate(MainWindow, htmlhead/bodyp align\center\span style\ font-size:18pt;\视频2/span/p/body/html)) self.pushButton.setText(_translate(MainWindow, 复位)) self.pushButton_2.setText(_translate(MainWindow, 复位)) self.pushButton_4.setText(_translate(MainWindow, 开启)) self.label_4.setText(_translate(MainWindow, htmlhead/bodyp align\center\span style\ font-size:18pt;\⚠/span/p/body/html)) self.label_5.setText(_translate(MainWindow, htmlhead/bodyp align\center\span style\ font-size:18pt;\⚠/span/p/body/html)) self.label_6.setText(_translate(MainWindow, htmlhead/bodyp align\center\span style\ font-size:18pt;\⚠/span/p/body/html)) self.pushButton_3.setText(_translate(MainWindow, 报警信息))2.执行代码import cv2 from collections import Counter import os from numpy import ndarray import sys sys.path.append(rC:\Program Files (x86)\MVS\Development\Samples\Python\MvImport) from HKCamera_class import HKCamera from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtCore import * from PyQt5.QtGui import * from PyQt5.QtWidgets import QFileDialog, QMainWindow from 金寨窗口0 import Ui_MainWindow import logging import numpy as np from paddleocr import PaddleOCR import re import serial import time import argparse import threading def release_capture3(cap): # 定期释放usb摄像头资源 while True: # 每隔一段时间释放一次资源这里设置为 30min time.sleep(1800) cap.release() cap.open(opt.cap_numb3) parser argparse.ArgumentParser() parser.add_argument(--SERIAL_PORT1, typestr, defaultCOM5, help第一个报警器的串口号) parser.add_argument(--SERIAL_PORT2, typestr, defaultCOM8, help第二 三个报警器的串口号) parser.add_argument(--confid_level, typefloat, default0.88, help识别的置信度) parser.add_argument(--cap_numb3, typeint, default1, help第三个摄像头编号) parser.add_argument(--frame_delay, typeint, default67, help获取画面帧数的延时) opt parser.parse_args() ######## 指令声明 LIGHT_BUZZ1 0110001A000101CE18 # 闪光声音1 LIGHT_BUZZ2 0110001A0001040E1B # 闪光声音1 LIGHT 0110001A0001028E19 # 闪光 BUZZ1 0110001A0001034FD9 # 声音1 BUZZ2 0110001A000105CFDB # 声音2 BUZZ_CMD_CLOSE 0110001A0001000FD8 # 关闭声音和闪光 logging.disable(logging.DEBUG) # 关闭日志的输出 def sendCmdToDevice(cmd, ser): # 控制警报器 cmdd bytes.fromhex(cmd) ser.write(cmdd) def most_common_element(lst): # 提取列表中出现次数最多的字符 # 使用Counter统计每个元素出现的次数 count Counter(lst) # 找到出现次数最多的元素 most_common_item count.most_common(1)[0][0] return most_common_item def process_string(input_string): # 在ocr的结果中筛选出对应型号 aa[] # 用空格切分字符串 parts input_string.split() # 正则表达式1: 包含数字和字母或者纯数字 pattern_alphanumeric re.compile(r^(?.*[a-zA-Z])(?.*\d)[a-zA-Z\d-]{2,10}$) # 正则表达式2: 至少三个以上数字的纯数字 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 def set_bing(set_a): # 替换一些易错字符 resu set() for j in set_a: 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 def hide_label_and_send_cmd(label,serNone): # 隐藏警告符号并停止报警 label.setVisible(False) # 隐藏警告符号 sendCmdToDevice(BUZZ_CMD_CLOSE, ser) def In_which(text, hun): a 0 for i in hun: if text in i: a 1 return a def are(i): # 计算字符面积 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 PaddleOCR(use_angle_clsTrue,use_gpuTrue, langen) ocr2 PaddleOCR(use_angle_clsFalse, use_gpuTrue, langen) class PyQtMainEntry(QMainWindow, Ui_MainWindow): def __init__(self): super().__init__() self.setupUi(self) self.ser1 serial.Serial(opt.SERIAL_PORT1, 9600, timeout2.5) self.ser2 serial.Serial(opt.SERIAL_PORT2, 9600, timeout2.5) # self.ser3 serial.Serial(opt.SERIAL_PORT3, 9600, timeout2.5) self.label_4.setVisible(False) self.label_5.setVisible(False) self.label_6.setVisible(False) self.list_zong1 [] self.list_zong2 [] self.list_zong3 [] self.set_zong1 set() self.set_zong2 set() self.set_zong3 set() self.set_12hun set() self.guo [] self.list1 [] self.list2 [] self.list3 [] self.daan1 self.daan2 # self.showMaximized() # 第一个摄像头 self.camera1 HKCamera(CameraIp192.168.20.56) self.camera1.set_Value(param_typeenum_value, node_namePixelFormat, node_valueBayerGB8) self.camera1.set_Value(param_typeenum_value, node_nameGainAuto, node_valueContinuous) self.camera1.set_Value(param_typefloat_value, node_nameAcquisitionFrameRate, node_value15.0000) # self.camera1.set_Value(param_typeenum_value, node_nameExposureAuto, # node_valueContinuous) # 自动曝光 self.camera1.start_camera() # 第二个摄像头 self.camera2 HKCamera(CameraIp192.168.20.20) self.camera2.set_Value(param_typeenum_value, node_namePixelFormat, node_valueBayerGB8) self.camera2.set_Value(param_typeenum_value, node_nameGainAuto, node_valueContinuous) self.camera2.set_Value(param_typefloat_value, node_nameAcquisitionFrameRate, node_value15.0000) # self.camera2.set_Value(param_typeenum_value, node_nameExposureAuto, # node_valueContinuous) #自动曝光 # self.camera2.set_Value(param_typeenum_value, node_nameDecimationHorizontal, # node_value2) # self.camera2.set_Value(param_typeenum_value, node_nameDecimationVertical, # node_value2) self.camera2.start_camera() self.camera3 cv2.VideoCapture(opt.cap_numb3) # 启动后台线程来定期释放第三个摄像头的资源 release_thread2 threading.Thread(targetrelease_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) self._timer.setInterval(opt.frame_delay) self.frame_counter 0 # 统计画面的帧数 # self.pushButton_4.click() 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) # sendCmdToDevice(BUZZ_CMD_CLOSE, self.ser3) self.label_4.setVisible(False) self.label_5.setVisible(False) self.label_6.setVisible(False) def open_folder(self): folder_path rD:\MVS\MVS\Development\Samples\Python\shiyan\baojing QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path)) def clearSet1(self): # 复位按钮一 self.set_zong1.clear() # self.list_zong1.clear() self.list1.clear() sendCmdToDevice(BUZZ_CMD_CLOSE, self.ser1) self.label_4.setVisible(False) def clearSet2(self): # 复位按钮二 self.set_zong2.clear() # self.list_zong2.clear() self.list2.clear() sendCmdToDevice(BUZZ_CMD_CLOSE, self.ser2) self.label_5.setVisible(False) def execute_after_n_calls(n, w2set): def decorator(func): def wrapper(self, *args, **kwargs): wrapper.count 1 result func(self, *args, **kwargs) if wrapper.count % n 0: w2set(self) return result wrapper.count 0 return wrapper return decorator QtCore.pyqtSlot() def _queryFrame(self): try: if not self.camera3.grab(): print(No frame grabbed.) self.camera3.release() self.close() else: self.frame1: ndarray self.camera1.get_image() self.frame2: ndarray self.camera2.get_image() ret3, self.frame3 self.camera3.read() if not ret3: print(No frame retrieved.) # self.camera1.release() self.camera3.release() self.close() else: self.frame11 cv2.resize(self.frame1, (640, 480)) self.frame22 cv2.resize(self.frame2, (640, 480)) self.frame33 cv2.resize(self.frame3, (640, 480)) if ret3: 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) 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) 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: self._performOCR1() if self.frame_counter % 3 0: self._performOCR2() # self._performOCR1() # self._performOCR2() # self._performOCR3() if self.frame_counter % 3 0: self._performOCR3() except: pass这个项目展示了如何将PyQt、工业相机、OCR技术和串口通信结合起来构建一个实用的工业视觉系统。希望对从事类似项目的开发者有所启发。