0 项目背景
“六·一”儿童节到了,献上一个识物读英文的AI点读机作为一个节日礼物。

在这里插入图片描述
在完成前面几个“点读”相关项目后,我们会发现,其实从pipeline上看,要实现AI点读功能难度可能并不是特别大。但是要让“点读”能力具体落地实现,没有一个好看的GUI肯定是行不通。的——毕竟,我们不能整天用键盘控制功能的启停、模型的切换吧。

但是,在AI项目要落地实施的时候,我们会发现,制作一个GUI前端与开发pineline似乎不是一个流程,前者关注工程,后者聚焦算法。

其实,为已经实现的算法套上个好看的GUI一点都不难,本文就以AI识物功能开发为例,介绍从0到1实现GUI界面和功能的全流程,以期帮助更多的AI开发者,“包装”好自己的算法。

“点读”相关前置项目:

【PaddlePaddle+OpenVINO】打造一个指哪读哪的AI“点读机”
【PaddlePaddle+OpenVINO】AI“朗读机”诞生记
【PaddlePaddle+OpenVINO】电表检测识别模型的部署
1 物体识别点读pipeline
首先,我们可以在AI Studio上跑通要开发小项目的pipeline。来盘一下,实现AI识物需要哪些流程:

传入一个视频——可以是摄像头的视频流,也可以是视频文件
分类——可以走视频分类,也可以走抽帧的图像分类,但是从实现上来看,既然是“点读”,那一开始,我们可以先倾向静态图片的分类
调用分类模型,得到top1的识别结果
将识别结果送入语音合成模型,生成wav文件
把wav文件朗读出来
具体的流程上,聚焦于第2步,还可用有很多细化,比如类似前置项目【PaddlePaddle+OpenVINO】打造一个指哪读哪的AI“点读机”里,加上手势识别能力,圈选传入视频的目标区域,只对目标内的物体进行分类;又比如指定一段开始和结束时间,截取这段时间内的传入视频做分类……

当然,在本项目中,我们先基于比较简单的图片分类,做一个基线的pipeline,读者可以根据实际需要,举一反三,实现更加细致的识物点读场景。

1.1 环境安装
本项目需要用到PaddleClas的whl包和PaddleSpeech,先准备好相关环境。

In [16]
!pip install paddleclas==2.0.2
In [ ]
!git clone https://gitee.com/eurake/nltk_data.git
In [8]
!mv nltk_data nltk_data1
In [ ]
!mv nltk_data1/packages ~/nltk_data
In [17]
!pip install pytest-runner -i https://pypi.tuna.tsinghua.edu.cn/simple
In [18]
!pip install paddlespeech
1.2 Pipeline实现
1.2.1 基于PaddleClas的图像分类
飞桨图像识别套件PaddleClas是飞桨为工业界和学术界所准备的一个图像识别任务的工具集,助力使用者训练出更好的视觉模型和应用落地。

PaddleClas 支持 Python Whl 包方式进行预测,目前Whl包方式仅支持图像分类,本文使用的是paddleclas 2.0.2,它在Python代码中实现和使用效果如下:

In [6]
from paddleclas import PaddleClas
clas = PaddleClas(model_name=‘EfficientNetB0_small’)
infer_imgs=‘output_1653949354.jpg’
result = clas.predict(infer_imgs)
Inference models that Paddle provides are listed as follows:

{‘DeiT_base_distilled_patch16_224’, ‘InceptionV4’, ‘MobileNetV1_x0_5’, ‘MobileNetV3_large_x0_35’, ‘EfficientNetB3’, ‘ResNet50_vc’, ‘SE_ResNeXt101_32x4d’, ‘ShuffleNetV2_x1_5’, ‘EfficientNetB0_small’, ‘DenseNet201’, ‘SE_ResNeXt50_32x4d’, ‘ViT_base_patch16_224’, ‘HRNet_W30_C’, ‘SE_ResNet50_vd’, ‘ResNeXt101_32x8d_wsl’, ‘EfficientNetB4’, ‘Xception65_deeplab’, ‘HRNet_W32_C’, ‘InceptionV3’, ‘DeiT_base_distilled_patch16_384’, ‘ViT_base_patch32_384’, ‘ResNeXt50_vd_32x4d’, ‘ResNet34_vd’, ‘ResNeXt152_32x4d’, ‘MobileNetV2_x2_0’, ‘GhostNet_x1_0’, ‘SE_HRNet_W64_C_ssld’, ‘MobileNetV3_small_x1_25’, ‘SE_ResNet34_vd’, ‘ResNeSt50’, ‘ResNet101’, ‘SE_ResNet18_vd’, ‘DPN131’, ‘SE_ResNeXt50_vd_32x4d’, ‘ResNeXt101_32x32d_wsl’, ‘ResNet101_vd’, ‘MobileNetV1_ssld’, ‘DeiT_tiny_distilled_patch16_224’, ‘MobileNetV3_small_x0_5’, ‘SqueezeNet1_0’, ‘EfficientNetB7’, ‘DeiT_base_patch16_384’, ‘MobileNetV1_x0_25’, ‘ResNeXt152_64x4d’, ‘MobileNetV3_large_x0_75’, ‘DenseNet264’, ‘ResNeXt101_vd_32x4d’, ‘Res2Net50_14w_8s’, ‘ResNeXt101_vd_64x4d’, ‘DPN68’, ‘ResNet101_vd_ssld’, ‘MobileNetV3_large_x1_0’, ‘ShuffleNetV2_x0_33’, ‘DPN107’, ‘MobileNetV3_small_x0_35’, ‘ResNeXt152_vd_32x4d’, ‘Xception65’, ‘Fix_ResNet50_vd_ssld_v2’, ‘AlexNet’, ‘HRNet_W64_C’, ‘HRNet_W48_C’, ‘DeiT_small_distilled_patch16_224’, ‘MobileNetV3_small_x0_75’, ‘ResNet50’, ‘ViT_base_patch16_384’, ‘Res2Net50_26w_4s’, ‘HRNet_W18_C’, ‘DeiT_tiny_patch16_224’, ‘MobileNetV1_x0_75’, ‘ResNeXt101_64x4d’, ‘ShuffleNetV2_x1_0’, ‘ResNet50_vd_ssld_v2’, ‘MobileNetV3_large_x1_0_ssld’, ‘ViT_small_patch16_224’, ‘MobileNetV2_x0_75’, ‘ResNet50_vd’, ‘SqueezeNet1_1’, ‘ShuffleNetV2_x2_0’, ‘ShuffleNetV2_x0_5’, ‘DenseNet161’, ‘GhostNet_x1_3_ssld’, ‘ResNet50_ACNet_deploy’, ‘VGG13’, ‘DenseNet121’, ‘ResNet34_vd_ssld’, ‘ResNet18’, ‘MobileNetV3_large_x1_25’, ‘Res2Net200_vd_26w_4s_ssld’, ‘ResNet200_vd’, ‘ResNet18_vd’, ‘ViT_large_patch32_384’, ‘Res2Net101_vd_26w_4s’, ‘ResNeXt50_vd_64x4d’, ‘Res2Net50_vd_26w_4s_ssld’, ‘VGG11’, ‘EfficientNetB5’, ‘ResNeXt101_32x16d_wsl’, ‘GhostNet_x1_3’, ‘MobileNetV2_x0_5’, ‘ResNeXt101_32x48d_wsl’, ‘MobileNetV2’, ‘MobileNetV3_small_x1_0_ssld’, ‘ResNeXt50_32x4d’, ‘DPN98’, ‘DeiT_base_patch16_224’, ‘ResNeXt50_64x4d’, ‘ViT_large_patch16_224’, ‘ResNeXt152_vd_64x4d’, ‘ResNeXt101_32x4d’, ‘HRNet_W40_C’, ‘DenseNet169’, ‘GoogLeNet’, ‘DarkNet53’, ‘VGG16’, ‘Xception41’, ‘EfficientNetB0’, ‘ShuffleNetV2_x0_25’, ‘MobileNetV3_small_x1_0’, ‘Fix_ResNeXt101_32x48d_wsl’, ‘DeiT_small_patch16_224’, ‘HRNet_W44_C’, ‘Res2Net50_vd_26w_4s’, ‘GhostNet_x0_5’, ‘ResNet50_vd_v2’, ‘Xception71’, ‘EfficientNetB6’, ‘EfficientNetB1’, ‘ResNeSt50_fast_1s1x64d’, ‘HRNet_W18_C_ssld’, ‘SENet154_vd’, ‘Res2Net200_vd_26w_4s’, ‘MobileNetV2_ssld’, ‘DPN92’, ‘Res2Net101_vd_26w_4s_ssld’, ‘ViT_large_patch16_384’, ‘Xception41_deeplab’, ‘MobileNetV2_x1_5’, ‘ResNet50_vd_ssld’, ‘MobileNetV3_large_x0_5’, ‘ResNet152_vd’, ‘RegNetX_4GF’, ‘ResNet152’, ‘ResNet34’, ‘VGG19’, ‘EfficientNetB2’, ‘MobileNetV2_x0_25’, ‘HRNet_W48_C_ssld’, ‘MobileNetV1’, ‘ShuffleNetV2_swish’}

download https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/inference/EfficientNetB0_small_infer.tar to /home/aistudio/.paddleclas/inference_model/EfficientNetB0_small/EfficientNetB0_small_infer.tar
100%|██████████| 19.7M/19.7M [00:01<00:00, 18.5MiB/s]
process params are as follows:
Namespace(batch_size=1, cpu_num_threads=10, enable_mkldnn=False, enable_profile=False, gpu_mem=8000, image_file=‘’, ir_optim=True, is_preprocessed=False, label_name_path=‘/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/paddleclas/ppcls/utils/imagenet1k_label_list.txt’, model_file=‘/home/aistudio/.paddleclas/inference_model/EfficientNetB0_small/inference.pdmodel’, model_name=‘EfficientNetB0_small’, normalize=True, params_file=‘/home/aistudio/.paddleclas/inference_model/EfficientNetB0_small/inference.pdiparams’, pre_label_image=False, pre_label_out_idr=None, resize=224, resize_short=256, top_k=1, use_fp16=False, use_gpu=False, use_tensorrt=False)
top-1 result: {‘filename’: ‘output_1653949354.jpg’, ‘class_ids’: array([817]), ‘scores’: array([0.26850194], dtype=float32), ‘label_names’: [‘sports car, sport car’]}
In [15]

通过split函数获得预测top1结果的标签

result[0][‘label_names’][0].split(‘,’)[0]
‘sports car’
1.2.2 基于PaddleSpeech实现语音合成
In [2]
import paddle
from paddlespeech.cli import TTSExecutor

tts_executor = TTSExecutor()
wav_file = tts_executor(
text= ‘This is a’ + result[0][‘label_names’][0],
output=‘output.wav’,
am=‘fastspeech2_ljspeech’,
am_config=None,
am_ckpt=None,
am_stat=None,
spk_id=0,
phones_dict=None,
tones_dict=None,
speaker_dict=None,
voc=‘pwgan_ljspeech’,
voc_config=None,
voc_ckpt=None,
voc_stat=None,
lang=‘en’,
device=paddle.get_device())
print(‘Wave file has been generated: {}’.format(wav_file))
2 PyQt5界面的设计与开发
2.1 Qt Designer的应用
在PyQt中编写UI界面可以直接通过代码来实现,也可以通过Qt Designer来完成。Qt Designer的设计符合MVC的架构,其实现了视图和逻辑的分离,从而实现了开发的便捷。Qt Designer中的操作方式十分灵活,其通过拖拽的方式放置控件可以随时查看控件效果。Qt Designer生成的.ui文件(实质上是XML格式的文件)也可以通过pyuic5工具转换成.py文件。在这里插入图片描述
很显然,我们入门的时候,使用Qt Designer开发UI界面,能够大幅提升开发效率。

不过,现在PyQt5中已经不提供Qt Designer工具了,所以,我们开发的时候,除了安装PyQt5,建议同时安装PySide2,使用PySide2中提供的Qt Designer工具。

pip install pyqt5 -i https://pypi.tuna.tsinghua.edu.cn/simple

pip install pyside2 -i https://pypi.tuna.tsinghua.edu.cn/simple
如果我们使用的是Anaconda管理各种依赖,那么到目录 ~\kkcac.conda\envs(env_name)\Lib\site-packages\PySide2下是可以找到designer.exe可执行文件的。

在这里插入图片描述
双击designer.exe进入,可以从最通用的Main Window开始创建。在这里插入图片描述
接下来就是,简易GUI界面的开发了。

完成界面开发后,保存时会有一个.ui文件,接下来我们需要做的,就是将整个.ui文件转换为.py文件。

pyuic5 -o 输出python文件名.py 输入ui文件名.ui
转换后的GUI代码如下:

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName(“MainWindow”)
MainWindow.resize(800, 600)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(MainWindow.sizePolicy().hasHeightForWidth())
MainWindow.setSizePolicy(sizePolicy)
self.centralwidget = QtWidgets.QWidget(MainWindow)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.centralwidget.sizePolicy().hasHeightForWidth())
self.centralwidget.setSizePolicy(sizePolicy)
self.centralwidget.setObjectName(“centralwidget”)
self.Open = QtWidgets.QPushButton(self.centralwidget)
self.Open.setGeometry(QtCore.QRect(240, 510, 75, 41))
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.Open.sizePolicy().hasHeightForWidth())
self.Open.setSizePolicy(sizePolicy)
self.Open.setObjectName(“Open”)
self.Close = QtWidgets.QPushButton(self.centralwidget)
self.Close.setGeometry(QtCore.QRect(540, 510, 75, 41))
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.Close.sizePolicy().hasHeightForWidth())
self.Close.setSizePolicy(sizePolicy)
self.Close.setObjectName(“Close”)
self.radioButtonCam = QtWidgets.QRadioButton(self.centralwidget)
self.radioButtonCam.setGeometry(QtCore.QRect(50, 510, 150, 16))
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.radioButtonCam.sizePolicy().hasHeightForWidth())
self.radioButtonCam.setSizePolicy(sizePolicy)
self.radioButtonCam.setChecked(False)
self.radioButtonCam.setObjectName(“radioButtonCam”)
self.radioButtonFile = QtWidgets.QRadioButton(self.centralwidget)
self.radioButtonFile.setGeometry(QtCore.QRect(50, 540, 150, 16))
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.radioButtonFile.sizePolicy().hasHeightForWidth())
self.radioButtonFile.setSizePolicy(sizePolicy)
self.radioButtonFile.setChecked(True)
self.radioButtonFile.setObjectName(“radioButtonFile”)
self.DispalyLabel = QtWidgets.QLabel(self.centralwidget)
self.DispalyLabel.setGeometry(QtCore.QRect(30, 20, 740, 480))
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.DispalyLabel.sizePolicy().hasHeightForWidth())
self.DispalyLabel.setSizePolicy(sizePolicy)
self.DispalyLabel.setFrameShape(QtWidgets.QFrame.Box)
self.DispalyLabel.setFrameShadow(QtWidgets.QFrame.Plain)
self.DispalyLabel.setText(“”)
self.DispalyLabel.setObjectName(“DispalyLabel”)
self.Read = QtWidgets.QPushButton(self.centralwidget)
self.Read.setGeometry(QtCore.QRect(390, 510, 75, 41))
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.Read.sizePolicy().hasHeightForWidth())
self.Read.setSizePolicy(sizePolicy)
self.Read.setObjectName(“Read”)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 22))
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.menubar.sizePolicy().hasHeightForWidth())
self.menubar.setSizePolicy(sizePolicy)
self.menubar.setObjectName(“menubar”)
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.statusbar.sizePolicy().hasHeightForWidth())
self.statusbar.setSizePolicy(sizePolicy)
self.statusbar.setObjectName(“statusbar”)
MainWindow.setStatusBar(self.statusbar)

    self.retranslateUi(MainWindow)
    QtCore.QMetaObject.connectSlotsByName(MainWindow)

def retranslateUi(self, MainWindow):
    _translate = QtCore.QCoreApplication.translate
    MainWindow.setWindowTitle(_translate("MainWindow", "PaddlePaddle AI识物点读机"))
    self.Open.setText(_translate("MainWindow", "启动"))
    self.Close.setText(_translate("MainWindow", "关闭"))
    self.radioButtonCam.setText(_translate("MainWindow", "camera"))
    self.radioButtonFile.setText(_translate("MainWindow", "file"))
    self.Read.setText(_translate("MainWindow", "点读"))
    ```

2.2 PyQt5播放实时视频流或本地视频文件
首先我们明确“点读机”的需求有两种实现,接入实时视频流或对本地视频文件中经过的物体进行识别。

然后,当然是发挥万能的“百度”大法,定位到关键词,找找相关资料,看看有没有开发者先造过“轮子”。

比如说PyQt5播放实时视频流或本地视频文件这篇文章就写得相对清晰明了,我们可以拿来参考。

当然,我们的情况更复杂些,按钮看似多了几个,其实套路都是一样的。

几个要点就是,要设置信号槽和按钮的触发状态

    # 信号槽设置
    ui.Open.clicked.connect(self.Open)
    ui.Close.clicked.connect(self.Close)
    ui.Read.clicked.connect(self.Read)
    ui.radioButtonCam.clicked.connect(self.radioButtonCam)
    ui.radioButtonFile.clicked.connect(self.radioButtonFile)

    # 创建一个点读事件并设为未触发
    self.readEvent = threading.Event()
    self.readEvent.clear()

    # 创建一个关闭事件并设为未触发
    self.stopEvent = threading.Event()
    self.stopEvent.clear()

以及相应的控制函数

def Open(self):
    if not self.isCamera:
        self.fileName, self.fileType = QFileDialog.getOpenFileName(self.mainWnd, 'Choose file', '', '*.mp4')
        self.cap = cv2.VideoCapture(self.fileName)
        self.frameRate = self.cap.get(cv2.CAP_PROP_FPS)
    else:
        # 下面两种rtsp格式都是支持的
        # cap = cv2.VideoCapture("rtsp://admin:Supcon1304@172.20.1.126/main/Channels/1")
        self.cap = cv2.VideoCapture(0)

    # 创建视频显示线程
    th = threading.Thread(target=self.Display)
    th.start()

def Close(self):
    # 关闭事件设为触发,关闭视频播放
    self.stopEvent.set()

def Read(self):
    self.readEvent.set()

2.3 串入算法pipeline
在图像处理方面,这里比较简单,主要就是从视频中抽帧,获取到预测标签,再写回后面的图片上。

当然,也可以顺便计算下推理时间。

打上预测标签和处理速度

cv2.putText(frame, f"Inference result: {result[‘label_names’][0].split(‘,’)[0]} Inference time: ({fps:.1f} FPS)", (5,50), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (0, 0, 255), 2)
在语音合成方面,需要注意一个问题:PaddleSpeech只是生成语音合成后的.wav文件,并不会去播放它。我们在播放语音文件时,实际上是调用了playsound模块打开了音频文件。

因此,如果音频文件反复生成读取都是同一个名字,很容易就产生RuntimeError报错。具体信息如下:

RuntimeError: Error opening ‘C:\MachineLearning\SpotReads\output.wav’: System error.
所以,在操作的时候,生成不重复的保存文件名还是十分必要的。在本项目中,具体的做法就是引入了时间戳。主要代码如下:

save_time = str(int(time.time()))
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
cv2.imwrite(‘output_%s.jpg’ % save_time, frame)

infer_imgs=‘R-C.png’

save_name = ‘output_%s.wav’ % save_time

wav_file = tts_executor(
text=‘This is a’ + result[‘label_names’][0].split(‘,’)[0],
output=save_name,
am=‘fastspeech2_ljspeech’,
am_config=None,
am_ckpt=None,
am_stat=None,
spk_id=0,
phones_dict=None,
tones_dict=None,
speaker_dict=None,
voc=‘pwgan_ljspeech’,
voc_config=None,
voc_ckpt=None,
voc_stat=None,
lang=‘en’,
device=paddle.get_device())

playsound(save_name)
最后,我们将2.2和2.3章节的内容串起来,就得到了控制代码:

import cv2
import threading
from PyQt5.QtCore import QFile
from PyQt5.QtWidgets import QFileDialog, QMessageBox
from PyQt5.QtGui import QImage, QPixmap
import numpy as np
import paddle
from paddleclas import PaddleClas
from paddlespeech.cli import TTSExecutor
from playsound import playsound
import time
import paddlehub as hub

clas = PaddleClas(model_name=‘EfficientNetB0_small’)

tts_executor = TTSExecutor()
class Display:
def init(self, ui, mainWnd):
self.ui = ui
self.mainWnd = mainWnd

    # 默认视频源为本地文件
    self.ui.radioButtonCam.setChecked(False)
    self.isCamera = False

    # 信号槽设置
    ui.Open.clicked.connect(self.Open)
    ui.Close.clicked.connect(self.Close)
    ui.Read.clicked.connect(self.Read)
    ui.radioButtonCam.clicked.connect(self.radioButtonCam)
    ui.radioButtonFile.clicked.connect(self.radioButtonFile)

    # 创建一个点读事件并设为未触发
    self.readEvent = threading.Event()
    self.readEvent.clear()

    # 创建一个关闭事件并设为未触发
    self.stopEvent = threading.Event()
    self.stopEvent.clear()

def radioButtonCam(self):
    self.isCamera = True

def radioButtonFile(self):
    self.isCamera = False

def Open(self):
    if not self.isCamera:
        self.fileName, self.fileType = QFileDialog.getOpenFileName(self.mainWnd, 'Choose file', '', '*.mp4')
        self.cap = cv2.VideoCapture(self.fileName)
        self.frameRate = self.cap.get(cv2.CAP_PROP_FPS)
    else:
        # 下面两种rtsp格式都是支持的
        self.cap = cv2.VideoCapture(0)

    # 创建视频显示线程
    th = threading.Thread(target=self.Display)
    th.start()

def Close(self):
    # 关闭事件设为触发,关闭视频播放
    self.stopEvent.set()

def Read(self):
    self.readEvent.set()

def Display(self):
    # 初始化控制按钮设置
    self.ui.Open.setEnabled(False)
    self.ui.Close.setEnabled(True)
    self.ui.Read.setEnabled(False)

    while self.cap.isOpened():
        # 视频启动时,点读按钮切换到可触发状态
        self.ui.Read.setEnabled(True)
        ret, frame = self.cap.read()
        start = time.time()
        # RGB转BGR  
        result = clas.predict(frame)
        end = time.time()
        # 处理一帧所用的时间
        seconds = end - start  
        # 一秒钟可以处理多少帧
        fps = 1 / seconds 
        # 打上预测标签和处理速度
        cv2.putText(frame, f"Inference result: {result['label_names'][0].split(',')[0]} Inference time: ({fps:.1f} FPS)", (5,50), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (0, 0, 255), 2)
        frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)      
        img = QImage(frame.data, frame.shape[1], frame.shape[0], QImage.Format_RGB888)
        self.ui.DispalyLabel.setPixmap(QPixmap.fromImage(img))

        if self.isCamera:
            cv2.waitKey(1)
        else:
            cv2.waitKey(int(1000 / self.frameRate))

        # 判断点读事件是否已触发
        if True == self.readEvent.is_set():
            save_time = str(int(time.time()))
            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            cv2.imwrite('output_%s.jpg' % save_time, frame)
            # infer_imgs='R-C.png'
            save_name = 'output_%s.wav' % save_time

            # print(result['label_names'][0].split(',')[0])

            wav_file = tts_executor(
                text='This is a' + result['label_names'][0].split(',')[0],
                output=save_name,
                am='fastspeech2_ljspeech',
                am_config=None,
                am_ckpt=None,
                am_stat=None,
                spk_id=0,
                phones_dict=None,
                tones_dict=None,
                speaker_dict=None,
                voc='pwgan_ljspeech',
                voc_config=None,
                voc_ckpt=None,
                voc_stat=None,
                lang='en',
                device=paddle.get_device())

            playsound(save_name)
            # 点读事件置为未触发,清空显示label
            self.readEvent.clear()
            self.ui.DispalyLabel.clear()
            

        # 判断关闭事件是否已触发
        if True == self.stopEvent.is_set():
            # 关闭事件置为未触发,清空显示label
            self.stopEvent.clear()
            self.ui.DispalyLabel.clear()
            self.ui.Close.setEnabled(False)
            self.ui.Open.setEnabled(True)
            self.ui.Read.setEnabled(False)
            break

2.4 main()函数的细节
有了GUI和控制代码,剩下的就是写个main()函数,不过笔者的笔记本上,出现了PyQt5的GUI死活不能自适应的情况,而且显示分辨率也严重有误。

最后,发现解决办法是必须加一行强制分辨率转换代码QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)。

import sys
import DisplayUI
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5 import QtCore
from VideoDisplay import Display

if name == ‘main’:
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
app = QApplication(sys.argv)
mainWnd = QMainWindow()
ui = DisplayUI.Ui_MainWindow()

# 可以理解成将创建的 ui 绑定到新建的 mainWnd 上
ui.setupUi(mainWnd)

display = Display(ui, mainWnd)

mainWnd.show()

sys.exit(app.exec_())

3 总结
本文介绍了一个基于PaddleClas和PaddleSpeech结合PyQt5制作的简单AI识物点读机是如何实现的。

美中不足的是,因为调用的是PaddleClas的whl部署包,物体识别结果的发音是英文——当然,学英语还是可以的。

在下一步的项目完善中,读者可以尝试基于PP-ShiTu或者自己训练的分类模型,让这台“识物点读机”读出我们想听的语言,并进一步提升识别效果。

Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐