随着功能越来越多,代码也越来越多,为了让这个标注原型工具有始有终,给他加了两个按钮,打开图片文件,保存标注文件,代码也到了解耦的时候了,这次一共涉及到三个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('datalabellistbak.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_())