【基于 PyQt5 + PaddleOCR 的工业视觉型号检测系统开发】

目录

系统功能

技术栈

系统架构

1.硬件配置

2.软件架构

核心库安装

项目整体逻辑

完整代码逐段解析

模块1:导入所需库

模块2:辅助函数

[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 计算字符面积-过滤过小的无效识别结果)

模块3:初始化OCR识别对象

模块4:核心类

[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识别与结果处理)

模块5:程序入口-启动程序


在现代工业生产中,产品型号与包装不匹配是常见质量问题。传统人工检测效率低、易疲劳。本文分享一套三摄像头实时 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设计,无需自己设计,文末附简单说明)

项目整体逻辑

  1. 初始化:启动摄像头、报警器、PyQt5界面,设置相关参数(比如串口号、摄像头IP);

  2. 画面采集:3个摄像头实时采集画面,显示在PyQt5界面上;

  3. OCR识别:每隔一定帧数,对采集到的画面进行文字识别,筛选出产品/包装型号;

  4. 型号比对:将工业相机识别的产品型号,与USB相机识别的包装型号进行比对;

  5. 异常处理:比对不一致时,触发声光报警,自动保存异常截图,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步:画面采集→文字识别→结果比对→异常处理。只要将复杂流程拆解开来,逐模块学习、逐段调试,就能轻松上手、顺利复刻。​

本文从前期环境搭建、项目逻辑梳理,到代码逐段解析、实战避坑指南,再到补充资源、进阶方向,只需跟着步骤操作,就能复刻出属于自己的工业视觉型号检测系统。这里重点提醒:遇到报错不要慌,优先排查路径、版本、硬件连接这三大核心问题;代码无需死记硬背,理解每个模块的核心作用,才能举一反三,适配不同的工业检测场景。​

相关推荐
steven_yzx11 小时前
自动驾驶相机坐标系转换
人工智能·数码相机·自动驾驶
steven_yzx11 小时前
自动驾驶相机坐标系转换2
人工智能·数码相机·自动驾驶
steven_yzx12 小时前
什么是IPM
数码相机·自动驾驶
AGV算法笔记1 天前
CVPR 2025顶级SLAM论文精读:MASt3R-SLAM如何用单目相机实现实时稠密三维重建?
深度学习·数码相机·机器人视觉·slam·三维重建·agv
格林威1 天前
面阵相机 vs 线阵相机:堡盟与海康相机选型差异全解析 附C++ 实战演示
开发语言·c++·人工智能·数码相机·计算机视觉·视觉检测·工业相机
三维频道2 天前
工业级三维扫描实测:汽车灯具复杂结构件的全尺寸 3D 测量方案分析
java·人工智能·python·数码相机·3d·汽车·汽车轻量化制造
杀生丸学AI2 天前
【动态重建】SparseCam4D:基于稀疏相机实现时空一致的4D重建技术
数码相机·aigc·扩散模型·图像编辑·视觉大模型·点云分割
爱吃巧克力的程序媛2 天前
计算机图形学---如何理解模型矩阵、视图矩阵、投影矩阵
数码相机·线性代数·矩阵
轻口味2 天前
HarmonyOS 6 轻相机应用开发4:物品分类功能实现
数码相机·分类·harmonyos