With more and more functions, there are more and more codes. In order to make this labeling prototype tool complete, two buttons are added to it to open the image file and save the labeling file. It is time for the code to be decoupled. This time A total of three python files are involved. In fact, the UI and logic can be further decoupled. In addition, I was lazy in the end. The code for saving the annotation file has not been really completed. First, there have been more things recently, and first, it is not worth it. Put too much effort into a prototype, and the full version will not be released later.
So this prototype version of image annotation is also nearing completion.
ui_labelChoose.py, this file mainly implements the selection of right-click label labels, which is relatively simple and will not be repeated. This can be disassembled into two files to realize the separation of UI and business logic
# -*- 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, on the basis of the original, adds a fileInfo dictionary to record the name, length and width of each picture to be labeled, in order to facilitate the use in subsequent labeling files.
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 # Redefine QLabel to implement drawing events and various mouse events class MyLabel(QLabel): def __init__(self, parent=None): ''' :param parent: Initialize basic parameters ''' 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 # Add a list to store the coordinates of the label box self.bboxList = [] self.labelindex = 0 self.curChoosedbbox = [] self.curbboxindex = -1 self.deleteboxflag = False self.fileInfo={} # Mouse double-click event, select the marked box of the current coordinates # If it exists in multiple marked boxes, the latest marked one will be displayed # Then ask if you want to delete the label box # If you are sure to delete, delete the label box where the current coordinates are located def mouseDoubleClickEvent(self, event): x = event.pos().x() y = event.pos().y() self.curChoosedbbox = [] # If the label box has not been made, it will not be processed if self.bboxList == []: return else: # Use this to determine which annotation box the current double-click coordinates appear in, and the last annotation will be deleted first tempbboxlist = self.bboxList for index, bbox in enumerate(tempbboxlist): # Determine whether the coordinates are in the label box if bbox[0] <= x <= bbox[2] and bbox[1] <= y <= bbox[3]: # If it is, record the currently selected label box and the index number in the list self.curChoosedbbox = bbox self.curbboxindex = index # Draw for the first time, highlight the selected annotation box self. update() # Determine whether there is a selected label box if self.curChoosedbbox != []: reply = QMessageBox.question(self, "Warning!", "Do you want to delete the currently selected annotation box", 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 # trigger event on mouse click # Get the start position of the mouse event def mousePressEvent(self, event): # Set draw flag to True self.flag = True self.deleteboxflag = False # After restarting the click event, cancel the selected label box self.curChoosedbbox = [] self.x0 = event.pos().x() self.y0 = event.pos().y() # Mouse move event # Draw the rectangle frame during the mouse travel def mouseMoveEvent(self, event): if self.flag: self.x1RealTime = event.pos().x() self.y1RealTime = event.pos().y() self. update() # Mouse release event def mouseReleaseEvent(self, event): # Set draw flag to False self.flag = False self.x1 = event.pos().x() self.y1 = event.pos().y() # In this way, there is no need to draw the real-time frame self.x1RealTime = self.x0 self.y1RealTime = self.y0 # Correct the save event bug of clicking the mouse, when the start coordinate is equal to the end coordinate, or when it is a straight line, it will not respond if self.x0 == self.x1 or self.y0 == self.y1: return # Store the four coordinate axes of the label box to bboxList dialogChoooseLabel = 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() # draw event def paintEvent(self, event): super().paintEvent(event) painter = QPainter() # Increase drawing start and end time painter.begin(self) # Traverse the coordinate list of the label box stored before 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]) # draw the current callout box # Construct the starting coordinates, width and height of the rectangular frame 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 current = QRect(tempx0, tempy0, width, height) # Construct a QPainter to draw a rectangular frame painter.setPen(QPen(Qt.blue, 1, Qt.SolidLine)) painter. drawRect(current) # Determine whether there is a currently selected window, if there is, and it has not been deleted, it will be highlighted if self.curChoosedbbox != []: # If it is not currently a delete flag, highlight it # Otherwise, the label box will not be drawn 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() # save to bbox list 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, this interface is newly added, with a label area and two command buttons to implement a simple labeling system. In order to adapt to the initialization process after opening the file, some simple modifications have been made to the MyLabel class.
# -*- 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") # Add centered display self.label.setAlignment(Qt.AlignCenter) self.verticalLayout.addWidget(self.label) # add scroll bar 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", "Save")) self.pushButtonopen.setText(_translate("Form", "Open File")) 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() # Get the current directory of the system title = "Save annotation file format" 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 in the label # 2. The x-axis of the center point of the box marked by x_center # 3. The y-axis of the center point of the frame marked by y_center # 4. width The width of the picture to be marked opened in the marking software # 5. height The height of the picture to be marked opened in the marking software 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() # Get the current directory of the system title = "Select Image File" filt = "Picture files (*.bmp *.png *.jpg);; all files (*.*)" 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_())
Finally, welcome to pay attention to the official account: python and big data analysis
?