在工业检测、安防监控等场景中,多摄像头协同识别+异常报警是常见的需求。本文将详细介绍一个基于PyQt5、OpenCV、PaddleOCR和串口通信的多摄像头识别报警系统,该系统可实现3路摄像头(2路工业相机+1路USB相机)的实时采集、OCR型号识别、异常情况判断及报警器联动控制,完整还原项目实现过程,供大家参考学习。

一、项目核心需求

本项目主要实现以下核心功能,适配工业场景下的自动化识别与报警需求:

  • 多摄像头协同采集:2路HK工业相机(固定IP)+1路USB相机,实时获取画面并在PyQt5界面展示;

  • OCR型号识别:基于PaddleOCR识别画面中的字符/型号,筛选有效信息并处理易错字符,提升识别准确率;

  • 异常判断与报警:对比识别结果与基准值,出现异常时触发报警器(闪光+声音),同时隐藏警告标识、保存异常画面;

  • 辅助功能:摄像头资源定期释放、界面控制(打开/关闭摄像头、复位、打开报警日志文件夹)、后台线程联动,确保系统稳定运行。

二、核心技术栈

项目依赖的技术框架与第三方库如下,需提前安装配置:

  • 界面开发:PyQt5(实现主窗口、摄像头画面展示、按钮控制、输入框显示);

  • 摄像头采集:OpenCV(USB相机采集与画面处理)、HKCamera_class(HK工业相机控制,需导入对应SDK);

  • 字符识别:PaddleOCR(中英文识别,启用GPU加速提升识别速度);

  • 串口通信:pyserial(与报警器联动,发送控制指令);

  • 其他:numpy(数值计算)、collections.Counter(统计识别结果频次)、threading(后台线程,释放摄像头资源)、logging(日志控制)。

三、项目整体结构

项目代码采用模块化设计,核心分为5个部分,逻辑清晰、易于维护,整体结构如下:

  1. 导入依赖与参数配置:导入所需库、配置命令行参数(串口、置信度、摄像头编号等);

  2. 工具函数定义:实现报警器控制、识别结果处理、字符面积计算等通用功能;

  3. OCR初始化:创建PaddleOCR实例,配置GPU加速与语言类型;

  4. 主窗口类(PyQtMainEntry):核心模块,实现摄像头初始化、画面采集、OCR识别、报警联动、界面交互;

  5. 程序入口:初始化PyQt应用,启动主窗口。

四、核心代码解析

以下针对关键模块、核心函数进行详细解析,结合代码逻辑说明实现思路,重点讲解难点与易错点。

4.1 参数配置与指令声明

通过argparse模块配置可自定义参数,方便后续部署时调整,无需修改代码;同时声明报警器控制指令(十六进制),用于串口通信。

import cv2
from collections import Counter
import os
from numpy import ndarray
import sys
sys.path.append(r"D:\MVS\MVS\Development\Samples\Python\MvImport")  # 导入HK相机SDK路径
from HKCamera_class import HKCamera
from PyQt5 import QtCore, QtGui, QtWidgets
# 省略其他导入...

# 命令行参数配置
parser = argparse.ArgumentParser()
parser.add_argument("--SERIAL_PORT1", type=str, default='COM5', help='第一个报警器的串口号')
parser.add_argument("--SERIAL_PORT2", type=str, default='COM4', help='第二、三个报警器的串口号')
parser.add_argument("--confid_level", type=float, default=0.88, help='识别的置信度')
parser.add_argument("--cap_numb3", type=int, default=1, help='第三个摄像头编号')
parser.add_argument("--frame_delay", type=int, default=67, help='获取画面帧数的延时')
opt = parser.parse_args()

# 报警器控制指令(十六进制转字节发送)
LIGHT_BUZZ1 = "0110001A000101CE18"  # 闪光+声音1
LIGHT_BUZZ2 = "0110001A0001040E1B"  # 闪光+声音2(注:代码中注释与指令一致,可根据实际需求调整)
LIGHT = "0110001A0001028E19"  # 仅闪光
BUZZ1 = "0110001A0001034FD9"  # 仅声音1
BUZZ2 = "0110001A000105CFDB"  # 仅声音2
BUZZ_CMD_CLOSE = "0110001A0001000FD8"  # 关闭声音和闪光

易错点:SDK路径需根据实际安装位置调整,否则会导致HKCamera_class导入失败;报警器指令需与硬件对应,错误指令会导致报警器无响应。

4.2 核心工具函数

工具函数封装通用功能,降低代码冗余,主要包括以下4个关键函数:

4.2.1 报警器控制函数(sendCmdToDevice)

将十六进制指令转换为字节,通过串口发送给报警器,实现报警控制。

def sendCmdToDevice(cmd, ser):   # 控制警报器
    cmdd = bytes.fromhex(cmd)  # 十六进制指令转字节
    ser.write(cmdd)  # 串口发送指令

4.2.2 识别结果处理函数(process_string、set_bing)

process_string:筛选OCR识别结果中的有效型号(含字母+数字、或4-7位纯数字);set_bing:替换易错字符(如0/O、s/5等),提升识别准确率。



def process_string(input_string):   # 在ocr的结果中筛选出对应型号
    aa=[]
    parts = input_string.split()  # 空格切分识别结果
    # 正则筛选:含字母+数字 或 4-7位纯数字
    pattern_alphanumeric = re.compile(r'^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z\d-]{2,10}$')
    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

4.2.3 字符面积计算函数(are)

计算OCR识别到的字符区域面积,用于过滤无效识别结果(面积过小的字符可能是干扰项)。

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
    return width * height  # 返回面积

4.2.4 后台线程函数(release_capture3)

USB相机长时间运行易出现资源占用问题,通过后台守护线程,每30分钟释放一次USB相机资源并重新打开,确保系统稳定。

def release_capture3(cap):    # 定期释放usb摄像头资源
    while True:
        time.sleep(1800)  # 每30分钟(1800秒)执行一次
        cap.release()  # 释放资源
        cap.open(opt.cap_numb3)  # 重新打开相机

4.3 主窗口类核心逻辑(PyQtMainEntry)

主窗口类是项目的核心,集成了摄像头初始化、画面采集、OCR识别、报警联动、界面交互等所有功能,重点解析以下关键方法。

4.3.1 初始化方法(__init__)

完成界面初始化、摄像头配置、串口初始化、线程启动等操作,是程序启动的入口。

class PyQtMainEntry(QMainWindow, Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.setupUi(self)  # 初始化PyQt界面(由Qt Designer生成)

        # 初始化串口(连接报警器)
        self.ser1 = serial.Serial(opt.SERIAL_PORT1, 9600, timeout=2.5)
        self.ser2 = serial.Serial(opt.SERIAL_PORT2, 9600, timeout=2.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 = []

        # 初始化HK工业相机(1号、2号相机,固定IP)
        self.camera1 = HKCamera(CameraIp='192.168.20.30')
        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()  # 启动相机

        self.camera2 = HKCamera(CameraIp='192.168.20.20')
        # 2号相机配置与1号一致,省略重复代码...
        self.camera2.start_camera()

        # 初始化USB相机(3号相机)
        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)  # 定时器触发画面采集
        self._timer.setInterval(opt.frame_delay)

        self.frame_counter = 0    # 统计画面帧数(用于控制OCR识别频率)

关键说明:HK工业相机的参数(PixelFormat、GainAuto等)需根据相机型号调整;定时器间隔(frame_delay)决定画面采集频率,影响系统性能与识别实时性。

4.3.2 画面采集与OCR识别(_queryFrame)

定时器触发的核心方法,实现3路摄像头画面采集、缩放、显示,并控制OCR识别频率(每隔一定帧数执行一次OCR,避免占用过多资源)。

@QtCore.pyqtSlot()
def _queryFrame(self):
    try:
        if not self.camera3.grab():  # 检查USB相机是否能获取画面
            print("No frame grabbed.")
            self.camera3.release()
            self.close()
        else:
            # 采集3路相机画面
            self.frame1: ndarray = self.camera1.get_image()
            self.frame2: ndarray = self.camera2.get_image()
            ret3, self.frame3 = self.camera3.read()

            if not ret3:  # USB相机采集失败处理
                print("No frame retrieved.")
                self.camera3.release()
                self.close()
            else:
                # 缩放画面(适配界面显示,640*480)
                self.frame11 = cv2.resize(self.frame1, (640, 480))
                self.frame22 = cv2.resize(self.frame2, (640, 480))
                self.frame33 = cv2.resize(self.frame3, (640, 480))

                # 画面转换为PyQt可显示的格式,并显示在界面标签上
                qimage = cv2.cvtColor(self.frame11.copy(), cv2.COLOR_BGR2RGB)
                qimage = QtGui.QImage(qimage.data, qimage.shape[1], qimage.shape[0], QtGui.QImage.Format_RGB888)
                self.label.setPixmap(QtGui.QPixmap.fromImage(qimage))
                # 2号、3号相机画面显示,省略重复代码...

                # 控制OCR识别频率(每隔指定帧数执行,避免频繁识别)
                self.frame_counter += 1
                if self.frame_counter % 5 == 0:
                    self._performOCR1()  # 1号相机OCR识别
                if self.frame_counter % 3 == 0:
                    self._performOCR2()  # 2号相机OCR识别
                    self._performOCR3()  # 3号相机OCR识别
    except:
        pass

4.3.3 OCR识别与异常判断(_performOCR系列+w2set系列)

_performOCR1/2/3:分别对3路相机画面执行OCR识别,筛选有效结果并存储;w2set1/2/3:对识别结果进行统计分析,判断是否异常,触发报警。

以1号相机为例(2号、3号逻辑类似,仅参数不同):

# 装饰器:控制OCR识别后,每执行3次调用w2set1(统计分析+异常判断)
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

@execute_after_n_calls(3, w2set1)
def _performOCR1(self):
    result = ocr2.ocr(self.frame1, cls=False)  # OCR识别(不进行角度校正)
    if result and not None in result:
        try:
            for i in result[0]:
                mianji = are(i)  # 计算字符面积
                ma = process_string(i[1][0])  # 筛选有效型号
                # 过滤条件:置信度>0.92、有有效型号、字符坐标在指定范围、面积>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)  # 取出现次数最多的作为基准值

            # 定期清空识别结果列表,避免数据冗余
            if self.frame_counter % 16 == 0:
                self.list1.clear()

            # 在界面输入框显示识别结果
            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:
                # 异常处理:保存异常画面、显示警告标识、触发报警器
                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()

关键说明:3号相机额外增加了颜色检测(牛皮纸颜色),只有当颜色占比≥20%时才执行OCR识别,进一步过滤无效画面,提升识别准确性。

4.4 界面交互方法

实现界面按钮的点击事件,包括摄像头打开/关闭、复位、打开报警日志文件夹等功能,提升系统易用性。

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)

def open_folder(self):
    # 打开报警日志文件夹(预设路径)
    folder_path = r'D:\MVS\MVS\Development\Samples\Python\shiyan\baojing'
    QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path))

def clearSet1(self):     # 1号相机复位按钮
    self.set_zong1.clear()
    self.list1.clear()
    sendCmdToDevice(BUZZ_CMD_CLOSE, self.ser1)
    self.label_4.setVisible(False)

# 2号相机复位按钮(clearSet2),逻辑与clearSet1一致,省略代码...

五、项目测试与注意事项

5.1 测试步骤

  1. 配置依赖环境,安装所需第三方库,确保HK相机SDK路径正确;

  2. 连接硬件(工业相机、USB相机、报警器),确认串口端口、相机IP与代码一致;

  3. 运行代码,点击“打开”按钮,查看3路相机画面是否正常显示;

  4. 放置含目标型号的物体,测试OCR识别是否准确,异常情况是否触发报警;

  5. 测试复位、关闭摄像头、打开日志文件夹等功能是否正常。

5.2 注意事项

  • SDK路径、相机IP、串口端口、报警日志路径等,需根据实际环境修改,否则会导致程序运行失败;

  • PaddleOCR启用GPU加速需配置对应环境(如CUDA),无GPU可将use_gpu=False,识别速度会略有下降;

  • OCR识别的置信度、字符面积阈值、帧数间隔等参数,需根据实际场景调试,提升识别准确率;

  • 串口通信时,需确保报警器与电脑串口连接正常,波特率(9600)与代码一致;

  • 长时间运行时,需注意电脑资源占用,可适当调整画面分辨率、OCR识别频率。

六、项目总结

本项目实现了多摄像头协同识别与报警的完整功能,基于PyQt5打造了简洁易用的界面,结合OpenCV、PaddleOCR实现了高效的画面采集与字符识别,通过串口通信实现了报警器联动,可直接适配工业检测、安防监控等场景。

项目的核心亮点的是:多摄像头协同工作、易错字符处理提升识别准确率、后台线程释放资源确保系统稳定、界面交互友好且可灵活配置参数。后续可根据实际需求,优化识别算法、增加画面标注、完善日志记录等功能。

后续将补充硬件信息,完善项目测试细节,如有疑问或优化建议,欢迎在评论区交流~

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐