本文主要是介绍【搞事情】利用PyQt为目标检测SSD300添加界面(四),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
【原创文章】欢迎正常授权转载(联系作者)
【反对恶意复制粘贴,如有发现必维权】
【微信公众号原文传送门】
这篇文章将详细介绍利用多进程的实现—方案3(代码获取见文章末尾)。相比之前的稍微复杂一点,先看看demo的最终效果(视频)。
1 需求分析
首先看一下UI界面,界面上各个控件的详细信息如下表所示。
控件序号 | 控件的类别 | Qobject_name | 功能 |
---|---|---|---|
1 | QLabel | label_imgshow | 实时显示视频 |
2 | QLabel | label_imgshaow_res | 显示抽帧检测效果 |
3 | QTextEdit | textEdit | 显示检测的目标信息 |
4 | QPushButton | pushButton_open | 打开视频文件 |
5 | QLineEdit | lineEdit_cameraIndex | 设置视频流URL;支持IP摄像头;数字为当前设备中摄像头索引(例:本人笔记本自带摄像头为0) |
6 | QPushButton | pushButton_start | 开始检测并播放 |
7 | QPushButton | pushButton_pause | 暂停检测及播放 |
8 | QPushButton | pushButton_end | 停止检测及播放(全部重设) |
结合上面的控件分析一下需求。方案3最初的设计需求是电脑的性能有限,无法做到实时检测并显示,我们希望做一个折中,不检测视频流中的每一帧图像,只是从中抽取部分来检测,检测时不打断画面的实时显示,在"后台"中尽可能多的检测视频流中的图像。之前的文章中也介绍过神经网络的预测过程是无法在线程中实现的,因此设计了一个如下图所示的方案,将图像检测神经网络放到一个子进程中负责在“后台”检测目标,主进程(UI进程)负责采集图像并实时显示在相应的控件上。
在实际的实现过程中面临以下几个关键点。
(1) 主进程(UI进程)采集到的图像数据如何传递给子进程检测?
Python多进程之间有一些简单的通讯方式,例如:Queue,好处是它是一个进程安全的队列,用户不需要关注变量的管理,但是实际用这种方式来传递图片,你就会发现速度慢呀!简直崩溃,最简单有效的还是通过“共享内存”,速度快很多(但是我还是觉得慢,我觉得主要是数据转来转去导致的),但需要对内存进行管理。
(2) 子进程检测的结果如何告知主进程并将结果显示出来?
最简单的就和上面一样,再使用一块“共享内存”来将绘制好检测结果的图片传递回主进程中,但是这显然不是最有效率的做法,毕竟使用“共享内存”耗时也挺长的,同时图片数据也挺大的,检测结果其实就是几个简单的数,没必要将整个图像都传递回去。我的处理方式是:主进程在“抽帧”时保存一个图像备份,子进程通过“Queue”将检测结果(相对图片数据小的多)传递回主进程,主进程收到结果后,将结果绘制在备份图像上并显示出来。
(3) 共享内存的管理。
使用共享内存时一定要注意这部分内存的管理,不能主进程“写”的同时你子进程在"读",否则数据不就错了嘛。严格点来说这里需要一个“互斥锁”(有兴趣的同学可以试试),我实在是比较懒,不想研究,直接创建了一个状态变量(“共享的”)来控制,主进程和子进程在读写“共享内存”前,通过判断状态变量的值来确定是否有“权利”使用该“共享内存”。
2 代码详解
(1) 构造函数
这部分需要注意的是:建立共享内存、检测子进程、消息接收线程,代码里面有详细的注释,这里不赘述。
def __init__(self, parent=None):super(MainWindow, self).__init__(parent)self.setupUi(self)
# 图像大小self.img_shape = (480, 720)
# 初始化界面self.label_imgshow.setScaledContents(True) # 图片自适应显示self.label_imgshow_res.setScaledContents(True) # 检测结果图片自适应显示self.img_none = np.ones((480, 720, 3), dtype=np.uint8)*255self.show_img(self.img_none)
# SSD检测初始化self.weight_path = './ssd/weights/weights_SSD300.hdf5'self.weight_path = os.path.normpath(os.path.abspath(self.weight_path))self.obj_names = ['Aeroplane', 'Bicycle', 'Bird', 'Boat', 'Bottle','Bus', 'Car', 'Cat', 'Chair', 'Cow', 'Diningtable','Dog', 'Horse', 'Motorbike', 'Person', 'Pottedplant','Sheep', 'Sofa', 'Train', 'Tvmonitor']# 需要显示的目标list, 用于过滤self.include_class = self.obj_names# -----------检测子进程--------------# 子进程返回结果使用self.queue = Queue()# 多进程之间的共享图片内存,参数‘I’表示数据类型为 int # 后一个参数为内存的大小,这里Python不提供多个维度数据的共享内存# 只有数组类型满足使用需求,因此主进程中需先将图像数据变为数组的样子,在子进程中再恢复self.img_share = RawArray('I', self.img_shape[0] * self.img_shape[1] * 3)# 标识当前进程的状态,非0:保持检测;0:停止检测self.process_flg = RawValue('I', 1)# 当前图像共享内存 img_share 的状态,非0:主进程使用中;0:子进程使用中self.img_get_flg = RawValue('I', 1)# 创建检测子进程self.detector_process = Process(target=detector_process,args=(self.img_share,self.img_shape,self.process_flg,self.img_get_flg,self.queue))self.detector_process.start() # 进程开始
# -------------------------------------------------------------
# -----------接收检测结果的线程--------------# 主要考虑到Queue的get方法可能会阻塞,如果直接在计时器函数中调用# get会导致UI“假死”,卡着不动。# 虽然也可以设置阻塞时间,但是建议还是建立线程接收处理Queue中的结果# 接收检测结果的线程self.recv_thread = Recv_res(parent=self, queue=self.queue)# 连接信号 # 这个信号用于通知UI响应显示,接收线程中只负责接收转发结果,后面代码中有详细介绍self.recv_thread.res_signal.connect(self.show_res)self.recv_thread.start()
# -------------------------------------------------------------
# 视频文件路径self.camera_index = 0self.FPS = None
# 初始化计时器self.timer = QTimer(self) # 更新计时器self.timer.timeout.connect(self.timer_update) # 超时信号连接对应的槽函数
# 等待加载模型self.textEdit.setText('正在加载模型,请稍后......')self.pushButton_start.setEnabled(False)self.pushButton_open.setEnabled(False)self.pushButton_pause.setEnabled(False)self.lineEdit_cameraIndex.setEnabled(False)
# 暂停初始化为不暂停self.pause = False
(2) 检测子进程目标函数
下面是检测子进程的目标函数,检测子进程开始后执行的就是这个函数,首先是初始化SSD并加载权重,之后进入帧循环检测,子进程的消息(包括检测结果)通过Queue传递返回,包括两部分:状态量和消息内容,设置状态量的目的是为了下一步针对不同的消息做相应的处理。
def detector_process(img_share, img_shape, process_flg, img_get_flg, res_queue):"""SSD检测子进程目标函数:param img_share: 待检测图像数据,共享内存:param img_shape: 待检测图像的大小 (h, w):param process_flg: 子进程状态量 0:退出检测进程;非0:保持检测:param img_get_flg: 共享图像内存 的状态量 0:子进程占用共享内存 1:主进程占有内存:param res_queue: 返回检测结果的通道:return:"""# 初始化SSDweight_path = './ssd/weights/weights_SSD300.hdf5'weight_path = os.path.normpath(os.path.abspath(weight_path))obj_names = ['Aeroplane', 'Bicycle', 'Bird', 'Boat', 'Bottle','Bus', 'Car', 'Cat', 'Chair', 'Cow', 'Diningtable','Dog', 'Horse', 'Motorbike', 'Person', 'Pottedplant','Sheep', 'Sofa', 'Train', 'Tvmonitor']include_class = obj_namesssd = SSD_test(weight_path=weight_path, class_nam_list=obj_names)
# 通知UI 模型加载成功res_queue.put((3, '模型加载成功'))
# 构建检测循环while True:# print('process_flg:{} img_get_flg:{}'.format(process_flg.value, img_get_flg.value))# 判断检测器状态,是否退出if process_flg.value == 0:print('安全退出检测进程!')res_queue.put((0, '检测进程已安全退出!'))break
# 判断共享内存当前状态是否可以安全读取数据if img_get_flg.value == 0:# print('开始检测!')try:img = np.array(img_share[:], dtype=np.uint8)img_scr = np.reshape(img, (img_shape[0], img_shape[1], 3))
# SSD检测preds = ssd.Predict(img_scr)# 结果过滤preds = filter(obj_names, preds, inclued_class=include_class)
h, w = img_shape[:2]res = decode_preds(obj_names, preds, w=w, h=h) # 列表
# 管道返回检测结果res_queue.put((1, res))
except:print('图片检测失败')res_queue.put((2, '当前图像检测失败!'))finally:# 释放图像共享内存占用,让主进程写入新的图像img_get_flg.value = 1
(3) 消息接收线程类
线程创建后会先进入构造函数,启动后执行run函数。主要的功能就是接收子进程通过Queue传递回来的消息,并通知UI做出相应的处理。接收到检测子进程退出的消息后,该线程也跳出循环结束生命周期。
class Recv_res(QThread):"""检测结果接收线程"""res_signal = pyqtSignal(list)@debug_class('Recv_res')def __init__(self, parent, queue:Queue):"""构造函数:param parent: 父实例 QObj ,Qt中父实例析构相应的子线程会安全退出,不用人工处理:param queue: 管道"""super(Recv_res, self).__init__(parent=parent)self.queue = queue
def run(self):while True:flg, res = self.queue.get()print(flg, res)if flg == 0: # 对应检测子进程已安全退出print('接收线程已安全退出!')self.res_signal.emit([0, res])breakelse:self.res_signal.emit([flg, res])
(4) 计时器超时槽函数
该函数主要是按时读取视频流中的图像并显示在控件上,每次判断检测共享内存的状态,如果子进程释放则将当前帧数据写入共享内存中,之后改变状态变量的值(释放对共享内存的占有)。
def timer_update(self):"""计时器槽函数:return:"""if self.cap.isOpened():# 读取图像ret, self.img_scr = self.cap.read()# ### 视频读取完毕if not ret:# 计时器停止计时self.timer.stop()# 不检测self.img_get_flg.value = 1# 对话框提示QMessageBox.information(self, '播放提示', '视频已播放完毕!')# 释放摄像头if hasattr(self, 'cap'):self.cap.release()del self.cap# 释放‘开始’按钮self.pushButton_start.setEnabled(True)# 禁止暂停并初始化其功能self.pause = Falseself.pushButton_pause.setText('暂停')self.pushButton_pause.setEnabled(False)# 释放视频流选择self.pushButton_open.setEnabled(True)self.lineEdit_cameraIndex.setEnabled(True)return
# 图像预处理self.img_scr = cv2.resize(self.img_scr, (self.img_shape[1], self.img_shape[0]))# 转为RGBself.img_scr = cv2.cvtColor(self.img_scr, cv2.COLOR_BGR2RGB)
if hasattr(self, 'detector_process'):# ### 抽帧if self.img_get_flg.value == 1:# print('开始抽帧')self.img_temp = self.img_scr.copy() # 用于显示检测结果self.img_share[:] = self.img_scr.reshape(-1).tolist() # 抽帧保存在中间缓存self.img_get_flg.value = 0 # 不再抽取 直到检测完成# print('结束抽帧')
# 显示图像self.show_img(self.img_scr)
# 响应UIQApplication.processEvents()else:self.textEdit.setText('数据流未打开!!!\n请检查')self.resst_detector()
(5) 窗口关闭事件函数
在关闭窗口之前需要关闭子进程,否则子进程会一直在后台运行,开一次软件创建一个,多次重复后电脑越来越卡。打开任务管理器后后发现有好多名叫“Python”的进程,这些就是创建后却没关闭的子进程。因此,在窗口关闭事件函数下改变检测进程的状态变量值,使子进程能够正常退出。
def closeEvent(self, a0):"""关闭窗口时间函数:param a0::return:"""self.process_flg.value = 0 # 退出子进程self.detector_process.join()
其他函数就不写了,非常简单。
由于本人能力有限,欢迎批评指正。
可以加我的QQ(1152291782)交流,请注明来意。
关注下方公众号,回复关键字即可获取下载地址。
-
本文配套源代码下载地址:
回复“SSD界面3”获取。
如果你读后有收获,欢迎关注我的微信公众号
上面有更多完全免费教程,我也会不定期更新
ღ ღ ღ 打开微信扫描下方二维码关注 ღ ღ ღ
这篇关于【搞事情】利用PyQt为目标检测SSD300添加界面(四)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!