前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >图像标注版本5终版-多标注框+标注标签+高亮和删除标签+打开图片文件+保存标注格式

图像标注版本5终版-多标注框+标注标签+高亮和删除标签+打开图片文件+保存标注格式

作者头像
python与大数据分析
发布2023-09-03 21:17:24
3150
发布2023-09-03 21:17:24
举报

随着功能越来越多,代码也越来越多,为了让这个标注原型工具有始有终,给他加了两个按钮,打开图片文件,保存标注文件,代码也到了解耦的时候了,这次一共涉及到三个python文件,其实还可以将UI和逻辑做进一步解耦,另外最后也懒了,关于保存标注文件的代码并未真正完成,一来最近事情多了起来,一来不值得为一个原型投入太多精力,后面完整版的也不会发出来。

所以这个图像标注原型版本也接近了尾声。

ui_labelChoose.py,这个文件主要实现右键标注标签的选择,比较简单不再重复,这个可以拆解为两个文件,实现UI和业务逻辑的分离

代码语言:javascript
复制
# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'ui_labelchoose.ui'
#
# Created by: PyQt5 UI code generator 5.15.4
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_Dialog(object):
    def setupUi(self, Dialog):
        Dialog.setObjectName("Dialog")
        Dialog.resize(285, 336)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(Dialog.sizePolicy().hasHeightForWidth())
        Dialog.setSizePolicy(sizePolicy)
        Dialog.setMinimumSize(QtCore.QSize(285, 336))
        Dialog.setMaximumSize(QtCore.QSize(285, 336))
        self.buttonBox = QtWidgets.QDialogButtonBox(Dialog)
        self.buttonBox.setGeometry(QtCore.QRect(80, 39, 193, 28))
        self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
        self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok)
        self.buttonBox.setObjectName("buttonBox")
        self.leditChoosedLabel = QtWidgets.QLineEdit(Dialog)
        self.leditChoosedLabel.setGeometry(QtCore.QRect(11, 11, 261, 21))
        self.leditChoosedLabel.setObjectName("leditChoosedLabel")
        self.leditChoosedLabel.setEnabled(False)
        self.lviewLabelList = QtWidgets.QListView(Dialog)
        self.lviewLabelList.setGeometry(QtCore.QRect(10, 80, 261, 241))
        self.lviewLabelList.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
        self.lviewLabelList.setObjectName("lviewLabelList")

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

    def retranslateUi(self, Dialog):
        _translate = QtCore.QCoreApplication.translate
        Dialog.setWindowTitle(_translate("Dialog", "Dialog"))

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QMainWindow, QApplication,  QDialog,QMessageBox
from PyQt5.QtCore import QStringListModel

class DialogChoooseLabelWin(QDialog, Ui_Dialog):
    def __init__(self,parent=None):
        # super(DialogChoooseLabelWin, self).__init__()
        QDialog.__init__(self,parent)
        self.setupUi(self)
        self.labelList=[]
        self.initLableList()
        self.lviewLabelList.clicked.connect(self.clickedlist)
        self.buttonBox.accepted.connect(self.validate)
        self.buttonBox.rejected.connect(self.reject)

    def initLableList(self):
        with open('data\labellistbak.txt', 'r',encoding='utf-8') as f:
            self.labelList=[line.strip() for line in f]
        self.labelslm=QStringListModel()
        self.labelslm.setStringList(self.labelList)
        self.lviewLabelList.setModel(self.labelslm)

    def clickedlist(self, qModelIndex):
        self.leditChoosedLabel.setText(self.labelList[qModelIndex.row()])

    def getValue(self):
        return self.leditChoosedLabel.text()

    def validate(self):
        if self.leditChoosedLabel.text()!='':
            self.accept()

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    Dialog=DialogChoooseLabelWin()
    print('dialogChooseLabel.exec_()=', Dialog.exec_())
    print('dialogChooseLabel.getValue()=', Dialog.getValue())
    sys.exit(app.exec_())

MyLabel.py,在原来基础上增加了一个fileInfo的字典,记录每次待标注图片的名称和长宽,为了便于后续标注文件中使用。

代码语言:javascript
复制
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QWidget, QApplication, QLabel, QMessageBox,QPushButton
from PyQt5.QtCore import QRect, Qt
from PyQt5.QtGui import QPixmap, QPainter, QPen
from ui_labelchoose import DialogChoooseLabelWin
import sys


# 重定义QLabel,实现绘制事件和各类鼠标事件
class MyLabel(QLabel):
    def __init__(self, parent=None):
        '''
        :param parent:
        初始化基本参数
        '''
        super(MyLabel, self).__init__(parent)
        self.initParam()

    def initParam(self):
        self.x0 = 0
        self.y0 = 0
        self.x1 = 0
        self.y1 = 0
        self.x1RealTime = 0
        self.y1RealTime = 0
        self.rect = QRect()
        self.flag = False
        # 增加一个存储标注框坐标的列表
        self.bboxList = []
        self.labelindex = 0
        self.curChoosedbbox = []
        self.curbboxindex = -1
        self.deleteboxflag = False
        self.fileInfo={}

    # 鼠标双击事件,选中当前坐标的被标注框
    # 如存在在多个被标注框内,则显示最新标注的那个
    # 再询问是否要删除标注框
    # 如果确定要删除,则删除当前坐标所在的标注框
    def mouseDoubleClickEvent(self, event):
        x = event.pos().x()
        y = event.pos().y()
        self.curChoosedbbox = []
        # 如果尚未做标注框,则不处理
        if self.bboxList == []:
            return
        else:
            # 以此判断当前双击坐标出现在哪个标注框中,最后标注的优先删除
            tempbboxlist = self.bboxList
            for index, bbox in enumerate(tempbboxlist):
                # 判断坐标是否在标注框中
                if bbox[0] <= x <= bbox[2] and bbox[1] <= y <= bbox[3]:
                    # 如果在的话,记录当前选中的标注框和list中的索引号
                    self.curChoosedbbox = bbox
                    self.curbboxindex = index
                    # 第一次绘制,高亮显示被选中的标注框
                    self.update()
                    # 判断是否已有选中的标注框
            if self.curChoosedbbox != []:
                reply = QMessageBox.question(self, "警告!", "是否要删除当前选中的标注框",
                                             QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                                             QMessageBox.StandardButton.Yes)
                if reply == QMessageBox.Yes:
                    self.deleteboxflag = True
                    self.bboxList.pop(self.curbboxindex)
                    self.update()
                else:
                    return

    # 单击鼠标触发事件
    # 获取鼠标事件的开始位置
    def mousePressEvent(self, event):
        # 将绘制标志设置为True
        self.flag = True
        self.deleteboxflag = False
        # 重新开始点击事件后,取消已选中的标注框
        self.curChoosedbbox = []
        self.x0 = event.pos().x()
        self.y0 = event.pos().y()

    # 鼠标移动事件
    # 绘制鼠标行进过程中的矩形框
    def mouseMoveEvent(self, event):
        if self.flag:
            self.x1RealTime = event.pos().x()
            self.y1RealTime = event.pos().y()
            self.update()

    # 鼠标释放事件
    def mouseReleaseEvent(self, event):
        # 将绘制标志设置为False
        self.flag = False
        self.x1 = event.pos().x()
        self.y1 = event.pos().y()
        # 这样就不用画出实时框了
        self.x1RealTime = self.x0
        self.y1RealTime = self.y0
        # 修正单击鼠标的保存事件bug,当开始坐标等于结束坐标时,或者为一条直线时,均不响应
        if self.x0 == self.x1 or self.y0 == self.y1:
            return
            # 将标注框的四个坐标轴存储到bboxList
        dialogChooseLabel = DialogChoooseLabelWin()
        if dialogChooseLabel.exec_():
            labelname = dialogChooseLabel.getValue()
            self.saveBBbox(self.x0, self.y0, self.x1, self.y1, labelname)
            # print('label rect=',self.x0, self.y0, self.x1, self.y1, labelname)
        event.ignore()

    # 绘制事件
    def paintEvent(self, event):
        super().paintEvent(event)
        painter = QPainter()
        # 增加绘制开始和结束时间
        painter.begin(self)
        # 遍历之前存储的标注框坐标列表
        for point in self.bboxList:
            rect = QRect(point[0], point[1], abs(point[0] - point[2]), abs(point[1] - point[3]))
            painter.setPen(QPen(Qt.red, 2, Qt.SolidLine))
            painter.drawRect(rect)
            painter.drawText(point[0], point[1], point[4])
        # 绘制当前标注框的举行
        # 构造矩形框的起始坐标和宽度、高度
        tempx0 = min(self.x0, self.x1RealTime)
        tempy0 = min(self.y0, self.y1RealTime)
        tempx1 = max(self.x0, self.x1RealTime)
        tempy1 = max(self.y0, self.y1RealTime)
        width = tempx1 - tempx0
        height = tempy1 - tempy0
        currect = QRect(tempx0, tempy0, width, height)
        # 构造QPainter,进行矩形框绘制
        painter.setPen(QPen(Qt.blue, 1, Qt.SolidLine))
        painter.drawRect(currect)

        # 判断是否有当前选中窗口,如果有,且未被删除,则高亮显示
        if self.curChoosedbbox != []:
            # 如果当前不是删除标志,则高亮显示
            # 否则就不再绘制该标注框
            if self.deleteboxflag == False:
                point = self.curChoosedbbox
                rect = QRect(point[0], point[1], abs(point[0] - point[2]), abs(point[1] - point[3]))
                painter.setPen(QPen(Qt.green, 4, Qt.SolidLine))
                painter.drawRect(rect)
                painter.drawText(point[0], point[1], point[4])

        painter.end()

    # 保存到bbox列表
    def saveBBbox(self, x0, y0, x1, y1, labelname):
        tempx0 = min(x0, x1)
        tempy0 = min(y0, y1)
        tempx1 = max(x0, x1)
        tempy1 = max(y0, y1)
        bbox = (tempx0, tempy0, tempx1, tempy1, labelname, self.labelindex)
        self.bboxList.append(bbox)
        self.labelindex += 1

labelannov5.py,这个界面是新增的,一个label区,两个命令按钮,实现一个简单的标注系统,为了适应打开文件后的初始化过程,也对MyLabel类做了一些简单修改。

代码语言:javascript
复制
# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'ui_tt.ui'
#
# Created by: PyQt5 UI code generator 5.15.4
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QApplication, QMainWindow,QWidget,QFileDialog,QScrollArea,QVBoxLayout
from PyQt5.QtGui import QPixmap, QPainter, QPen
from PyQt5.QtCore import QRect, Qt,QDir
from MyLabel import MyLabel
import sys,os

class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName("Form")
        Form.resize(960, 540)
        self.layoutWidget = QtWidgets.QWidget(Form)
        self.layoutWidget.setGeometry(QtCore.QRect(21, 11, 921, 521))
        self.layoutWidget.setObjectName("layoutWidget")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget)
        self.verticalLayout.setContentsMargins(0, 0, 0, 0)
        self.verticalLayout.setObjectName("verticalLayout")
        self.label = MyLabel(self.layoutWidget)
        self.label.resize(900,450)
        self.label.setObjectName("label")
        # 添加居中展示
        self.label.setAlignment(Qt.AlignCenter)

        self.verticalLayout.addWidget(self.label)

        # 添加滚动栏
        self.scroll_area = QScrollArea()
        self.scroll_area.setWidget(self.label)
        self.scroll_area.setWidgetResizable(True)
        self.verticalLayout.addWidget(self.scroll_area)

        self.pushButton = QtWidgets.QPushButton(self.layoutWidget)
        self.pushButton.setObjectName("pushButton")
        self.verticalLayout.addWidget(self.pushButton)
        self.pushButtonopen = QtWidgets.QPushButton(self.layoutWidget)
        self.pushButtonopen.setObjectName("pushButtonsave")
        self.verticalLayout.addWidget(self.pushButtonopen)

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

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "Form"))
        self.label.setText(_translate("Form", "TextLabel"))
        self.pushButton.setText(_translate("Form", "保存"))
        self.pushButtonopen.setText(_translate("Form", "打开文件"))


class MyMainWindow(QWidget, Ui_Form):
    def __init__(self, parent=None):
        super(MyMainWindow, self).__init__(parent)
        self.setupUi(self)
        self.initUI()
        self.pushButton.clicked.connect(self.onSave)
        self.pushButtonopen.clicked.connect(self.onOpen)

    def initUI(self):
        pass

    def onSave(self):
        curPath = QDir.currentPath()  # 获取系统当前目录
        title = "保存标注文件格式"
        filt = "Text Format (*.txt);;Json Format(*.json);;XML Format(*.XML)"
        saveFileName, flt = QFileDialog.getSaveFileName(self, title, curPath, filt)
        import os
        if saveFileName !='':
            fileName, suffixName = os.path.splitext(os.path.basename(saveFileName))
            if suffixName==".txt":
                self.savetoText(saveFileName)
            elif suffixName==".csv":
                self.savetoCSV(saveFileName)
            elif suffixName==".json":
                self.savetoJson(saveFileName)
            elif suffixName==".XML":
                self.savetoXML(saveFileName)
            else:
                pass
        else:
            return


    def savetoText(self,fileName):
        # 1、 标注中类 class
        # 2、x_center 标注的那个框框的中心点的x轴
        # 3、y_center 标注的那个框框的中心点的y轴
        # 4、width 标注软件中打开的准备被标注的图片的宽度
        # 5、height 标注软件中打开的准备被标注的图片的高度
        print('savetoText {}'.format(fileName))

    def savetoXML(self,fileName):
        # <annotation>
        #   <folder/>
        #   <filename>2011_000025.jpg</filename>
        #   <database/>
        #   <annotation/>
        #   <image/>
        #   <size>
        #     <height>375</height>
        #     <width>500</width>
        #     <depth>3</depth>
        #   </size>
        #   <segmented/>
        #   <object>
        #     <name>bus</name>
        #     <pose/>
        #     <truncated/>
        #     <difficult/>
        #     <bndbox>
        #       <xmin>84.0</xmin>
        #       <ymin>20.384615384615387</ymin>
        #       <xmax>435.0</xmax>
        #       <ymax>373.38461538461536</ymax>
        #     </bndbox>
        #   </object>
        #   <object>
        #     <name>bus</name>
        #     <pose/>
        #     <truncated/>
        #     <difficult/>
        #     <bndbox>
        #       <xmin>1.0</xmin>
        #       <ymin>99.0</ymin>
        #       <xmax>107.0</xmax>
        #       <ymax>282.0</ymax>
        #     </bndbox>
        #   </object>
        #   <object>
        #     <name>car</name>
        #     <pose/>
        #     <truncated/>
        #     <difficult/>
        #     <bndbox>
        #       <xmin>409.0</xmin>
        #       <ymin>167.0</ymin>
        #       <xmax>500.0</xmax>
        #       <ymax>266.0</ymax>
        #     </bndbox>
        #   </object>
        # </annotation>
        print('savetoXML {}'.format(fileName))

    def savetoJson(self,fileName):
        # [
        #     {
        #         "name": "235_2_t20201127123021723_CAM2.jpg",
        #         "image_height": 6000,
        #         "image_width": 8192,
        #         "category": 5,
        #         "bbox": [
        #             1876.06,
        #             998.04,
        #             1883.06,
        #             1004.04
        #         ]
        #     },
        #     {
        #         "name": "235_2_t20201127123021723_CAM2.jpg",
        #         "image_height": 6000,
        #         "image_width": 8192,
        #         "category": 5,
        #         "bbox": [
        #             1655.06,
        #             1094.04,
        #             1663.06,
        #             1102.04
        #         ]
        #     },
        #     {
        #         "name": "235_2_t20201127123021723_CAM2.jpg",
        #         "image_height": 6000,
        #         "image_width": 8192,
        #         "category": 5,
        #         "bbox": [
        #             1909.06,
        #             1379.04,
        #             1920.06,
        #             1388.04
        #         ]
        #     }
        # ]
        print('savetoJson {}'.format(fileName))

    def onOpen(self):
        curPath = QDir.currentPath()  # 获取系统当前目录
        title = "选择图片文件"
        filt = "图片文件(*.bmp *.png *.jpg);;所有文件(*.*)"
        fileName, flt = QFileDialog.getOpenFileName(self, title, curPath, filt)
        if (fileName == ""):
            return
        else:
            img = QPixmap(fileName)
            self.label.setPixmap(img)
            # self.label.setScaledContents(True)
            self.label.setCursor(Qt.CrossCursor)
            self.label.initParam()
            self.show()
            self.label.fileInfo={"picturefilename":fileName,
                                 "picturebasename":os.path.basename(fileName),
                                 "picturewidth":img.width(),
                                 "pictureheight":img.height()}


if __name__ == '__main__':
    app = QApplication(sys.argv)
    myWin = MyMainWindow()
    myWin.show()
    sys.exit(app.exec_())
本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-07-27,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 python与大数据分析 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
http://www.vxiaotou.com