手把手教你快速打造一个AI识物点读机

AI百科3周前更新 快创云
18 0

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/

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_all’)

infer_imgs=‘output_.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_all’, ‘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_all_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_all_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_all_x0_35’, ‘ResNeXt152_vd_32x4d’, ‘Xception65’, ‘Fix_ResNet50_vd_ssld_v2’, ‘AlexNet’, ‘HRNet_W64_C’, ‘HRNet_W48_C’, ‘DeiT_all_distilled_patch16_224’, ‘MobileNetV3_all_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_all_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_all_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_all_x1_0’, ‘Fix_ResNeXt101_32x48d_wsl’, ‘DeiT_all_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_all_infer.tar to /home/aistudio/.paddleclas/inference_model/EfficientNetB0_all/EfficientNetB0_all_infer.tar

100%|██████████| 19.7M/19.7M [0001<0000, 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_all/inference.pdmodel’, model_name=‘EfficientNetB0_all’, normalize=True, params_file=‘/home/aistudio/.paddleclas/inference_model/EfficientNetB0_all/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_.jpg’, ‘class_ids’ array([817]), ‘scores’ array([0.194], dtype=float32), ‘label_names’ [‘sports car, sport car’]}

In [15]

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 DesignerUI界面,能够大幅提升效率。

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

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

pip install pyside2 -i https//pypi.tuna.tsinghua.edu.cn/

如果我们使用的是Anaconda管理各种依赖,那么到目录 ~kkcac.condaenvs(env_name)Libsite-packagesPySide2下是可以找到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.statuar = QtWidgets.QStatusBar(MainWindow)

sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)

sizePolicy.setHorizontalStretch(0)

sizePolicy.setVerticalStretch(0)

sizePolicy.setHeightForWidth(self.statuar.sizePolicy().hasHeightForWidth())

self.statuar.setSizePolicy(sizePolicy)

self.statuar.setObjectName(“statuar”)

MainWindow.setStatusBar(self.statuar)

2.2 PyQt5播放实时视频流或本地视频文件

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

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

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

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

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

以及相应的控制函数

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 ‘CMachineLearningSpotReadsoutput.wav’ System error.

所以,在操作的时候,生成不重复的保存文件名还是十分必要的。在本项目中,具体的做法就是引入了时间戳。主要代码如下:

save_time = str(int(time.time()))

frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

cv2.imwrite(‘output_%s.jpg’ % save_time, frame)

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_all’)

tts_executor = TTSExecutor()

class Display

def init(self, ui, mainWnd)

self.ui = ui

self.mainWnd = mainWnd

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()

3 总结

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

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

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

© 版权声明

相关文章