基于机器视觉的工业产品型号识别与报警系统实现

在工业生产检测场景中,快速、准确识别产品型号并对异常型号及时报警,是保障产品质量和生产效率的关键环节。本文将分享一套基于 Python、OpenCV、PaddleOCR 和 PyQt5 实现的多摄像头产品型号识别与报警系统,该系统可实时采集多路摄像头画面,通过 OCR 识别产品型号,对比标准型号后触发声光报警,并保存异常画面。

一、系统整体架构

系统核心功能分为三大部分:

  1. 摄像头数据采集:支持海康工业相机(网口)和 USB 摄像头,实时获取多路画面;
  2. OCR 型号识别:基于 PaddleOCR 实现字符识别,针对工业场景字符易混淆问题做专项优化;
  3. 异常检测与报警:对比识别结果与基准型号,异常时触发声光报警并保存取证画面。

系统整体流程:

复制代码
摄像头画面采集 → 帧间隔OCR识别 → 字符清洗与标准化 → 型号对比 → 异常则声光报警+画面存档

二、核心技术选型

表格

技术 / 库 用途
PyQt5 可视化界面搭建,摄像头画面展示,按钮交互
OpenCV 图像预处理、摄像头数据读取、画面绘制
PaddleOCR 光学字符识别,支持中英文、角度分类
serial 串口通信,控制声光报警器
threading 后台线程管理,避免界面卡顿
numpy 图像数据处理、字符区域面积计算

三、核心代码实现

1. 环境准备

首先安装依赖包:

复制代码
pip install opencv-python pyqt5 paddleocr numpy pyserial

2. 系统初始化(核心类定义)

python 复制代码
import cv2
from collections import Counter
import os
from numpy import ndarray
import sys
import logging
import numpy as np
from paddleocr import PaddleOCR
import re
import serial
import time
import argparse
import threading
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import QFileDialog, QMainWindow

# 报警指令定义
LIGHT_BUZZ1 = "0110001A000101CE18"  # 闪光+声音1
LIGHT = "0110001A0001028E19"        # 仅闪光
BUZZ_CMD_CLOSE = "0110001A0001000FD8"  # 关闭报警

# 初始化OCR引擎
ocr = PaddleOCR(use_angle_cls=True,use_gpu=True, lang='en')
ocr2 = PaddleOCR(use_angle_cls=False, use_gpu=True, lang='en')

# 命令行参数解析
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()

class PyQtMainEntry(QMainWindow, Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        
        # 初始化串口(连接报警器)
        self.ser1 = serial.Serial(opt.SERIAL_PORT1, 9600, timeout=2.5)
        self.ser2 = serial.Serial(opt.SERIAL_PORT2, 9600, timeout=2.5)
        
        # 初始化UI元素(隐藏报警标签)
        self.label_4.setVisible(False)  # 摄像头1异常标签
        self.label_5.setVisible(False)  # 摄像头2异常标签
        self.label_6.setVisible(False)  # 摄像头3异常标签
        
        # 初始化数据存储容器
        self.list_zong1 = []  # 摄像头1识别结果列表
        self.list_zong2 = []  # 摄像头2识别结果列表
        self.list_zong3 = []  # 摄像头3识别结果列表
        self.set_zong1 = set()
        self.set_zong2 = set()
        self.set_zong3 = set()
        self.set_12hun = set()  # 摄像头1+2基准型号集合
        
        # 初始化摄像头
        # 海康工业相机1
        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()
        
        # 海康工业相机2
        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摄像头3
        self.camera3 = cv2.VideoCapture(opt.cap_numb3)
        
        # 启动后台线程定期释放USB摄像头资源(防止内存泄漏)
        release_thread2 = threading.Thread(target=self.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  # 帧数计数器

3. 关键工具函数

(1)串口指令发送(控制报警器)
python 复制代码
def sendCmdToDevice(cmd, ser):
    """向报警器发送串口指令"""
    cmdd = bytes.fromhex(cmd)
    ser.write(cmdd)

def hide_label_and_send_cmd(label,ser=None):
    """隐藏报警标签并停止报警"""
    label.setVisible(False)
    if ser:
        sendCmdToDevice(BUZZ_CMD_CLOSE, ser)
(2)字符标准化(解决易混淆字符问题)
python 复制代码
def set_bing(set_a):
    """替换工业场景易混淆字符:0→O、s→5、I→1等"""
    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 process_string(input_string):
    """筛选符合型号规则的字符(字母+数字/纯数字)"""
    aa=[]
    parts = input_string.split()
    # 正则1:包含字母+数字(2-10位)
    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}$')
    for part in parts:
        if pattern_alphanumeric.match(part) or pattern_at_least_two_digits.match(part):
            aa.append(part)
    return aa
(3)字符区域面积计算(筛选最大字符区域)
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))
    width = (width_A + width_B) / 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))
    height = (height_A + height_B) / 2
    
    area = width * height
    return area

4. 摄像头画面采集与 OCR 识别

python 复制代码
def _queryFrame(self):
    """定时器回调:采集摄像头画面并触发OCR"""
    try:
        if not self.camera3.grab():
            print("USB摄像头无画面")
            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 ret3:
                # 调整画面尺寸并显示到UI
                self.frame11 = cv2.resize(self.frame1, (640, 480))
                self.frame22 = cv2.resize(self.frame2, (640, 480))
                self.frame33 = cv2.resize(self.frame3, (640, 480))
                
                # 转换为Qt图像格式并显示
                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))
                
                qimage2 = cv2.cvtColor(self.frame22.copy(), cv2.COLOR_BGR2RGB)
                qimage2 = QtGui.QImage(qimage2.data, qimage2.shape[1], qimage2.shape[0], QtGui.QImage.Format_RGB888)
                self.label_3.setPixmap(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)
                self.label_2.setPixmap(QtGui.QPixmap.fromImage(qimage3))
                
                # 帧间隔触发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 Exception as e:
        print(f"画面采集异常:{e}")

# 装饰器:指定调用次数后执行数据处理
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, lambda self: self.w2set1())
def _performOCR1(self):
    """摄像头1 OCR识别"""
    result = ocr2.ocr(self.frame1, cls=False)
    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):
    """摄像头1识别结果处理:异常判断+报警"""
    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)  # 取出现次数最多的作为基准
            
            # 10秒刷新一次基准列表
            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(f"摄像头1处理异常:{e}")
        finally:
            self.list_zong1.clear()
            self.set_zong1.clear()

5. 系统运行入口

复制代码
if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    window = PyQtMainEntry()
    window.show()
    sys.exit(app.exec_())

四、系统亮点

  1. 多摄像头适配:同时支持工业网口相机和 USB 摄像头,满足不同场景需求;
  2. 字符鲁棒识别:针对工业场景易混淆字符(0/O、1/I/L、5/S 等)做专项替换,提升识别准确率;
  3. 性能优化:通过帧间隔 OCR、字符面积筛选、出现次数统计,降低计算量同时提升识别稳定性;
  4. 异常处理完善:异常时自动保存取证画面、触发声光报警,5 秒后自动复位,无需人工干预;
  5. 线程安全:后台线程管理摄像头资源,避免 UI 卡顿和资源泄漏。

五、扩展方向

  1. 模型优化:针对特定产品型号定制 PaddleOCR 训练数据集,进一步提升识别准确率;
  2. 远程监控:增加网络推送功能,将异常画面和报警信息推送到手机 / 电脑端;
  3. 数据统计:增加数据库存储功能,统计异常型号出现频次、报警时间等,辅助生产分析;
  4. 自适应调节:根据环境光照自动调整相机曝光参数,提升复杂环境下的识别稳定性。

该系统已在实际工业检测场景中落地应用,能够有效替代人工肉眼识别,提升检测效率和准确率,降低漏检、错检率。核心代码具备良好的可扩展性,可根据不同工业场景的需求快速适配调整。

相关推荐
gf13211114 小时前
python_更新飞书多维表格的单项关联字段
数据库·python·飞书
2601_953660374 小时前
File类
linux·开发语言·python
GIOTTO情4 小时前
Infoseek 媒介投放 API 实战:基于 Python 的全流程自动化方案摘要
开发语言·python·自动化
广州灵眸科技有限公司4 小时前
瑞芯微(EASY EAI)RV1126B 千兆以太网电路
服务器·前端·人工智能·python·深度学习
speop4 小时前
【thorough-pytorch】评价指标
人工智能·pytorch·python
YUDAMENGNIUBI4 小时前
day26_人工神经网络_多分类任务案例
python·神经网络
TechWayfarer4 小时前
街道级IP定位的技术边界:IP精准定位服务在本地生活场景的落地实践
大数据·网络·python·tcp/ip·生活
码界筑梦坊4 小时前
129-基于Python的城市尾气排放数据可视化分析系统
开发语言·python·信息可视化·数据分析·毕业设计
晚霞的不甘4 小时前
CANN昇腾 MindSpore 适配深入解析:如何在 MindSpore 框架中充分发挥昇腾硬件性能的完整指南
人工智能·python·transformer