【目标检测】YOLOv5-PyQT可视化例程开发

2022-10-31 18:33:35 浏览数 (2)

前言

花了几天功夫做了一个YOLOv5的PyQT可视化程序,主要针对多幅图片训练、自动标注和检测展示。涉及正在进行的项目,暂时不开源。在开发过程中,踩了不少坑,这里简单做一些记录。

项目使用到的开源代码: YOLOv5(5.0 6.0):https://github.com/ultralytics/yolov5 自动标注程序:https://github.com/cnyvfang/labelGo-Yolov5AutoLabelImg

效果演示

整体效果演示如下:

https://www.bilibili.com/video/av646659872

交互式目标检测软件演示

遇到的问题和解决方案

ui文件转py

使用QtDesigner设计的ui文件,可以通过PyUIC自动生成对应的py文件。

生成的文件仅包含窗体对象,浏览时,可以添加下方的执行程序:

代码语言:javascript复制
if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    Window = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(Window)
    Window.show()
    sys.exit(app.exec_())

pyinstaller多线程崩溃重启

在开发完之后,用pyinstaller打包应用程序,调用子线程训练时,卡在一半不动,然后程序崩溃自动重启。 查阅相关资料,在windows上Pyinstaller打包多进程程序需要添加代码使其支持多线程操作,添加代码如下:

代码语言:javascript复制
import multiprocessing

if __name__ == "__main__":
    multiprocessing.freeze_support()  # 支持pyinstaller,防止打包闪退

pyqt样式美化

pyqt样式美化有很多种开源css文件,本次开发中尝试了三种方案。

qdarkstyle

安装qdarkstyle:

代码语言:javascript复制
pip install qdarkstyle

qdarkstyle包括了深色和浅色两种主题,使用方式和效果如下:

深色主题:

代码语言:javascript复制
import qdarkstyle

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) # Qdark深色样例
    Window = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(Window)
    Window.show()
    sys.exit(app.exec_())

浅色主题

代码语言:javascript复制
from qdarkstyle.light.palette import LightPalette

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
	app.setStyleSheet(qdarkstyle.load_stylesheet(qt_api='pyqt5', palette=LightPalette()))  # Qdark浅色样例
    Window = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(Window)
    Window.show()
    sys.exit(app.exec_())

QCandyUi

QCandyUi安装:

代码语言:javascript复制
pip install QCandyUi

QCandyUi使用:

代码语言:javascript复制
from qt_material import apply_stylesheet

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    apply_stylesheet(app, theme='light_teal.xml')
    Window = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(Window)
    Window.show()
    sys.exit(app.exec_())

这里使用了light_teal.xml这个主题,还有其它主题可供选择:

代码语言:javascript复制
'dark_amber.xml',
'dark_blue.xml',
'dark_cyan.xml',
'dark_lightgreen.xml',
'dark_pink.xml',
'dark_purple.xml',
'dark_red.xml',
'dark_teal.xml',
'dark_yellow.xml',
'light_amber.xml',
'light_blue.xml',
'light_cyan.xml',
'light_cyan_500.xml',
'light_lightgreen.xml',
'light_pink.xml',
'light_purple.xml',
'light_red.xml',
'light_teal.xml',
'light_yellow.xml'

这些主题都比较简陋,基本上是换个颜色而已

飞扬青云-QSS

下面三套QSS样式取自知乎刘典武 为了方便下载,我上传到了资源中:https://download.csdn.net/download/qq1198768105/86775044

总共有三套样式,对应黑色、白色、蓝色,本项目使用的是lightblue这套样式。

使用方式:

代码语言:javascript复制
if __name__ == "__main__":
    multiprocessing.freeze_support()  # 支持pyinstaller,防止打包闪退
    app = QtWidgets.QApplication(sys.argv)
    styleFile = 'ui/lightblue.css'
    with open(styleFile, 'r') as f:
        qssStyle = f.read()
    app.setStyleSheet(qssStyle)
    Window = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi()
    ui.show()
    sys.exit(app.exec_())

pyqt基础操作

下面是项目中反复使用的一些基础函数和命令。

设置应用图标

代码语言:javascript复制
app.setWindowIcon(QIcon('ui/icon.png'))

设置按钮有效状态

代码语言:javascript复制
self.pushButton.setEnabled(True)

固定窗口尺寸

代码语言:javascript复制
self.setFixedSize(self.width(), self.height())

设置窗口背景渐变填充

代码语言:javascript复制
self.setStyleSheet('''
    #mainWindow{background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1,stop:0 rgb(255,255,255),stop:.99 rgb(151, 201, 242));}
''')

设置空间背景透明

代码语言:javascript复制
fill_bg = '''
    QWidget {
        background: transparent;
    }
'''
self.scroll.setStyleSheet(fill_bg)

打开子窗口

代码语言:javascript复制
def open_train_window(self):
    self.train_window = train_window.train_Window()
    self.train_window.show()

打开文件夹

代码语言:javascript复制
def open_output(self):
    path = os.getcwd()   '\'   'outputs'
    os.system(f"start explorer {path}")

子线程传参

子线程通过信号与槽函数机制来传递参数

代码语言:javascript复制
self.thread = DetectionThread(self)
self.thread.start()
self.thread.progressBarValue.connect(self.callback)

def callback(self, i):
    self.progressBar.setValue(i)

class DetectionThread(QThread):
    progressBarValue = pyqtSignal(int) 
    self.progressBarValue.emit(100)

获取文本框输入信息

代码语言:javascript复制
self.lineEdit.textChanged[str].connect(self.get_epoch)

def get_epoch(self, epoch):
    self.epoch = epoch

子目录添加路径

调用子目录中的程序,在import之前需添加根路径

代码语言:javascript复制
sys.path.append("yolov5")

获取当前根路径

代码语言:javascript复制
Root = os.path.split(os.path.abspath(__file__))[0]

缩略图列表显示

本项目开发之中,遇到的一个重点问题是在ListView中动态添加缩略图,其中分解成两个问题,一个是ListView的使用,另一个是动态缩略图的添加。

QScrollArea

ListView在pyqt中有个对应的控件是QScrollArea,找到了一个使用例程: 参考自:https://blog.csdn.net/Yibaomeimei/article/details/124694955

代码语言:javascript复制
import sys
from ui_test import *
from PyQt5.QtWidgets import *
import random


class test_ui(QMainWindow, Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        #生成随机的列表作为我们要显示的输出结果
        self.results = [[random.randint(1, 4) for j in range(1, 5)] for i in range(1, 15)]
        self.topFiller = QWidget()
        self.topFiller.setMinimumSize(500, 1000)
        for i in range(len(self.results)):
            label = QLabel(self.topFiller)
            label.resize(420, 30)
            label.setText(str("{:" "<13}".format(self.results[i][0])   ("{:" "<15}".format(self.results[i][1]))
                                "{:" "<13}".format(self.results[i][2])
                                "{:" "<13}".format(self.results[i][3])))
            label.move(10, 30 * i)
        self.vbox = QVBoxLayout()
        self.scroll = QScrollArea()
        self.scroll.setWidget(self.topFiller)
        self.vbox.addWidget(self.scroll)
        self.frame.setLayout(self.vbox)



if __name__ == "__main__":
    app = QApplication(sys.argv)
    mainwindow = test_ui()
    mainwindow.show()
    sys.exit(app.exec_())

缩略图加载显示

缩略图加载显示找到了一个例程: 参考自:https://blog.csdn.net/weixin_42512684/article/details/103414691

代码语言:javascript复制
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import os
import sys


class img_viewed(QWidget):
    def __init__(self,parent =None):
        super(img_viewed,self).__init__(parent)
        self.parent = parent
        self.width = 960
        self.height = 500

        self.scroll_ares_images = QScrollArea(self)
        self.scroll_ares_images.setWidgetResizable(True)

        self.scrollAreaWidgetContents = QWidget(self)
        self.scrollAreaWidgetContents.setObjectName('scrollAreaWidgetContends')

        # 进行网络布局
        self.gridLayout = QGridLayout(self.scrollAreaWidgetContents)
        self.scroll_ares_images.setWidget(self.scrollAreaWidgetContents)

        self.scroll_ares_images.setGeometry(20, 50, self.width, self.height)
        self.vertocal1 = QVBoxLayout()


        # self.meanbar = QMenu(self)
        # self.meanbar.addMenu('&菜单')
        # self.openAct = self.meanbar.addAction('&Open',self.open)
        # self.startAct =self.meanbar.addAction('&start',self.start_img_viewer)
        self.open_file_pushbutton =QPushButton(self)
        self.open_file_pushbutton.setGeometry(150,10,100,30)
        self.open_file_pushbutton.setObjectName('open_pushbutton')
        self.open_file_pushbutton.setText('打开文件夹...')
        self.open_file_pushbutton.clicked.connect(self.open)


        self.start_file_pushbutton = QPushButton(self)
        self.start_file_pushbutton.setGeometry(750, 10, 100, 30)
        self.start_file_pushbutton.setObjectName('start_pushbutton')
        self.start_file_pushbutton.setText('开始')
        self.start_file_pushbutton.clicked.connect(self.start_img_viewer)

        self.vertocal1.addWidget(self.scroll_ares_images)
        self.show()

        #设置图片的预览尺寸;
        self.displayed_image_size = 100
        self.col = 0
        self.row =0
        self.initial_path =None

    def open(self):
        file_path = QFileDialog.getExistingDirectory(self, '选择文文件夹', '/')
        if file_path ==None:
            QMessageBox.information(self,'提示','文件为空,请重新操作')
        else:
            self.initial_path =file_path

    def start_img_viewer(self):
        if self.initial_path:
            file_path = self.initial_path
            print('file_path为{}'.format(file_path))
            print(file_path)
            img_type = 'jpg'
            if file_path and img_type:

                png_list = list(i for i in os.listdir(file_path) if str(i).endswith('.{}'.format(img_type)))
                print(png_list)
                num = len(png_list)
                if num !=0:
                    for i in range(num):
                        image_id = str(file_path   '/'   png_list[i])
                        print(image_id)
                        pixmap = QPixmap(image_id)
                        self.addImage(pixmap, image_id)
                        print(pixmap)
                        QApplication.processEvents()
                else:
                    QMessageBox.warning(self,'错误','生成图片文件为空')
                    self.event(exit())
            else:
                QMessageBox.warning(self,'错误','文件为空,请稍后')
        else:

            QMessageBox.warning(self, '错误', '文件为空,请稍后')



    def loc_fil(self,stre):
        print('存放地址为{}'.format(stre))
        self.initial_path = stre

    def geng_path(self,loc):
        print('路径为,,,,,,{}'.format(loc))
    def gen_type(self,type):
        print('图片类型为:,,,,{}'.format(type))


    def addImage(self, pixmap, image_id):
        #图像法列数
        nr_of_columns = self.get_nr_of_image_columns()
        #这个布局内的数量
        nr_of_widgets = self.gridLayout.count()
        self.max_columns =nr_of_columns
        if self.col < self.max_columns:
            self.col =self.col  1
        else:
            self.col =0
            self.row  =1

        print('行数为{}'.format(self.row))
        print('此时布局内不含有的元素数为{}'.format(nr_of_widgets))

        print('列数为{}'.format(self.col))
        clickable_image = QClickableImage(self.displayed_image_size, self.displayed_image_size, pixmap, image_id)
        clickable_image.clicked.connect(self.on_left_clicked)
        clickable_image.rightClicked.connect(self.on_right_clicked)
        self.gridLayout.addWidget(clickable_image, self.row, self.col)


    def on_left_clicked(self,image_id):
        print('left clicked - image id = ' image_id)

    def on_right_clicked(self,image_id):
        print('right clicked - image id = '   image_id)


    def get_nr_of_image_columns(self):
        #展示图片的区域
        scroll_area_images_width = self.width
        if scroll_area_images_width > self.displayed_image_size:

            pic_of_columns = scroll_area_images_width // self.displayed_image_size  #计算出一行几列;
        else:
            pic_of_columns = 1
        return pic_of_columns

    def setDisplayedImageSize(self,image_size):
        self.displayed_image_size =image_size




class QClickableImage(QWidget):
    image_id =''

    def __init__(self,width =0,height =0,pixmap =None,image_id = ''):
        QWidget.__init__(self)

        self.layout =QVBoxLayout(self)
        self.label1 = QLabel()
        self.label1.setObjectName('label1')
        self.lable2 =QLabel()
        self.lable2.setObjectName('label2')
        self.width =width
        self.height = height
        self.pixmap =pixmap

        if self.width and self.height:
            self.resize(self.width,self.height)
        if self.pixmap:
            pixmap = self.pixmap.scaled(QSize(self.width,self.height),Qt.KeepAspectRatio,Qt.SmoothTransformation)
            self.label1.setPixmap(pixmap)
            self.label1.setAlignment(Qt.AlignCenter)
            self.layout.addWidget(self.label1)
        if image_id:
            self.image_id =image_id
            self.lable2.setText(image_id)
            self.lable2.setAlignment(Qt.AlignCenter)
            ###让文字自适应大小
            self.lable2.adjustSize()
            self.layout.addWidget(self.lable2)
        self.setLayout(self.layout)

    clicked = pyqtSignal(object)
    rightClicked = pyqtSignal(object)

    def mousePressEvent(self,ev):
        print('55555555555555555')
        if ev.button() == Qt.RightButton:
            print('dasdasd')
            #鼠标右击
            self.rightClicked.emit(self.image_id)
        else:
            self.clicked.emit(self.image_id)

    def imageId(self):
        return self.image_id



if __name__ =='__main__':
    app =QApplication(sys.argv)
    windo = img_viewed()
    windo.show()
    sys.exit(app.exec_())

开发中基本沿用了这套加载方案,将原始的网格布局改成垂直盒布局,然后每读取一张图片将其实例化,先使用pixmap.scaled缩放到合适尺寸,再使用addWidget添加到布局中。然后对每一张图片设置了mousePressEvent点击事件监听,点击之后替换主页面图片。

cfg文件的读写

项目中,需要获取子线程中检测进度,将其实时传递到主线程中,进行进度条更新。然而,子线程运行的是另一个子文件夹中的py程序,使用了全局变量、公共对象等方法均没成功。最后想到一个文件读写的方式,单独通过一个cfg文件来传递参数。

下面提供一个示例,包括cfg文件的读取和写入操作:

代码语言:javascript复制
import configparser
from configparser import ConfigParser

CONFIG_FILE = "glovar.cfg"
process = "1"



if __name__ == "__main__":
    # 将conf对象中的数据写入到文件中
    conf = configparser.ConfigParser()
    cfg_file = open("glovar.cfg", 'w')
    conf.add_section("default")  # 在配置文件中增加一个段
    # 第一个参数是段名,第二个参数是选项名,第三个参数是选项对应的值
    conf.set("default", "process", process)
    conf.write(cfg_file)
    cfg_file.close()
    # 读取cfg数据
    config_parser = ConfigParser()
    config_parser.read('glovar.cfg')
    config = config_parser['default']
    print(config['process'])

附录:Coco数据集类别

视频演示中,使用了Coco数据集中的图片进行演示,代码中需要写出其80个类别:

英文类别:

代码语言:javascript复制
names: [ 'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light',
         'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow',
         'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee',
         'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard',
         'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple',
         'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch',
         'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone',
         'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear',
         'hair drier', 'toothbrush' ]

中文类别:

代码语言:javascript复制
names = ["人", "自行车", "汽车", "摩托车", "飞机", "公交车",
         "火车", "卡车", "船", "交通灯", "消防栓",
         "停止标志", "停车计时器", "长凳", "鸟", "猫", "狗",
         "马", "羊", "牛", "大象", "熊", "斑马", "长颈鹿",
         "背包", "雨伞", "手提包", "领带", "手提箱", "飞盘", "滑雪",
         "滑雪板", "体育用球", "风筝", "棒球棒", "棒球手套",
         "滑板", "冲浪板", "网球拍", "瓶子", "红酒杯", "杯子",
         "叉子", "小刀", "勺子", "碗", "香蕉", "苹果", "三明治",
         "橘子", "西兰花", "胡萝卜", "热狗", "披萨", "甜甜圈", "蛋糕",
         "椅子", "沙发", "盆栽", "床", "餐桌", "厕所", "显示器",
         "笔记本", "鼠标", "遥控器", "键盘", "手机", "微波炉", "烤箱",
         "吐司机", "水槽", "冰箱", "书", "闹钟", "花瓶", "剪刀",
         "玩具熊", "吹风机", "牙刷"]

txt格式:

代码语言:javascript复制
person
bicycle
car
motorbike
aeroplane
bus
train
truck
boat
traffic light
fire hydrant
stop sign
parking meter
bench
bird
cat
dog
horse
sheep
cow
elephant
bear
zebra
giraffe
backpack
umbrella
handbag
tie
suitcase
frisbee
skis
snowboard
sports ball
kite
baseball bat
baseball glove
skateboard
surfboard
tennis racket
bottle
wine glass
cup
fork
knife
spoon
bowl
banana
apple
sandwich
orange
broccoli
carrot
hot dog
pizza
donut
cake
chair
sofa
pottedplant
bed
diningtable
toilet
tvmonitor
laptop
mouse
remote
keyboard
cell phone
microwave
oven
toaster
sink
refrigerator
book
clock
vase
scissors
teddy bear
hair drier
toothbrush

0 人点赞