Based on PyQt5 to develop image annotation version 5 final version – multiple annotation boxes + annotation labels + highlight and delete labels + open image files + save annotation format…

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.

5b1baf8c0e004dd680b6fa3e30f64818.png

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

?