基于YOLOv8模型训练之图像特征标注程序

部署运行你感兴趣的模型镜像

一、引言

在着手训练自定义模型的过程中,我们不可避免地需要准备大量的图像(image)及其对应的标签(label)信息。这些标签的精准程度,对于模型的精确率(Precision)、准确率(Accuracy)以及F1值等关键性能指标具有直接且深远的影响。鉴于此,本人专门设计并开发了一款图像特征标注程序,旨在高效辅助模型的训练工作。
程序无偿分享,欢迎修改指正!

二、UI 界面

Index page
label

三、功能简介

该程序的主要功能包括:通过选择图片文件夹来预览图像;在设置标注类型后(再次启动程序后无需重新设置,对图片中的特征进行标注并记录;完成每张图片的标注后,点击保存并继续下一张图片的标注工作;同时,程序还支持对标签类型进行修改。这些功能共同构成了该图像特征标注程序的核心用途。

1. 核心类库

PySide6:负责程序的界面的搭建;
python-opencv:负责每张图片的标注;

2. 图像特征标注

首先,我创建了一个名为ImageLabel的类,专门负责处理每张图片的显示(800x600 图片尺寸较大将会被缩放到规定尺寸)及其特征标注功能。该类默认采用矩形框作为标注特征的方式,并通过监听鼠标事件来精确捕捉并标注图片内的特征点。

        # 将 OpenCV 图像转换为 QImage
        height, width, channel = self.scaled_image.shape
        bytes_per_line = 3 * width
        q_img = QImage(self.scaled_image.data, width, height, bytes_per_line, QImage.Format_RGB888).rgbSwapped()

        # 绘制标注
        painter = QPainter(q_img)
        pen = QPen(QColor(255, 0, 0), 2)  # 红色画笔
        painter.setPen(pen)
        for annotation in self.annotations:
            shape, label = annotation
            if isinstance(shape, QRect):  # 矩形框
                painter.drawRect(shape)
                painter.drawText(shape.topLeft(), label)
            elif isinstance(shape, QPolygon):  # 多边形
                painter.drawPolygon(shape)
                painter.drawText(shape.boundingRect().topLeft(), label)
            elif isinstance(shape, QPoint):  # 点标注
                painter.drawPoint(shape)
                painter.drawText(shape, label)
        if self.current_shape:
            if isinstance(self.current_shape, QRect):  # 当前绘制的矩形框
                painter.drawRect(self.current_shape)
            elif isinstance(self.current_shape, QPolygon):  # 当前绘制的多边形
                painter.drawPolygon(self.current_shape)
            elif isinstance(self.current_shape, QPoint):  # 当前绘制的点
                painter.drawPoint(self.current_shape)
        painter.end()

        # 显示图像
        self.setPixmap(QPixmap.fromImage(q_img).scaled(self.max_size, Qt.KeepAspectRatio, Qt.SmoothTransformation))

3. 图像特征存储

通过捕获鼠标的位置坐标,确定矩形框的左上顶点坐标和右下端点坐标。随后,创建一个与图片具有相同名称的 .txt 文件,并将所选的标记类型(例如“stone”)以及对应的标记框信息写入该文件中,每一行记录一个标记框的信息。

    def save_annotations(self):
        """保存标注结果"""
        if not self.image_label.annotations:
            print("没有标注可保存")
            return

        if self.current_image_path is None:
            print("未加载图片")
            return

        # 默认保存路径为图片的同路径
        image_name = os.path.splitext(os.path.basename(self.current_image_path))[0]
        txt_path = os.path.join(self.image_folder, f"{image_name}_annotations.txt")

        # 保存标注信息到 TXT 文件
        with open(txt_path, "w") as f:
            for shape, label in self.image_label.annotations:
                if isinstance(shape, QRect):  # 矩形框
                    f.write(f"{label} {shape.left()} {shape.top()} {shape.right()} {shape.bottom()}\n")
                elif isinstance(shape, QPolygon):  # 多边形
                    points = ", ".join([f"{point.x()} {point.y()}" for point in shape])
                    f.write(f"{label} {points}\n")
                elif isinstance(shape, QPoint):  # 点标注
                    f.write(f"{label} {shape.x()} {shape.y()}\n")
        print(f"标注已保存到: {txt_path}")

四、核心代码

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("简易图像标注工具")
        self.setGeometry(100, 100, 800, 600)

        # 主布局
        self.central_widget = QWidget()
        self.setCentralWidget(self.central_widget)
        self.layout = QVBoxLayout(self.central_widget)

        # 标注类型选择
        self.annotation_type_combo = QComboBox()
        self.annotation_type_combo.addItems(["矩形框", "多边形", "点标注"])
        self.annotation_type_combo.currentTextChanged.connect(self.set_annotation_type)
        # self.layout.addWidget(self.annotation_type_combo)

        # 图片显示区域和切换按钮布局
        self.image_layout = QHBoxLayout()

        # 上一张按钮
        self.prev_button = QPushButton("上一张 (A)")
        self.prev_button.setEnabled(False)  # 初始状态禁用
        self.prev_button.setShortcut(QKeySequence("A"))  # 添加快捷键 A
        self.image_layout.addWidget(self.prev_button)

        # 图像显示区域
        self.image_label = ImageLabel(self)  # 将 MainWindow 实例传递给 ImageLabel
        self.image_layout.addWidget(self.image_label)

        # 下一张按钮
        self.next_button = QPushButton("下一张 (D)")
        self.next_button.setEnabled(False)  # 初始状态禁用
        self.next_button.setShortcut(QKeySequence("D"))  # 添加快捷键 D
        self.image_layout.addWidget(self.next_button)

        self.layout.addLayout(self.image_layout)

        # 当前图片索引和总图片数显示
        self.index_label = QLabel("0/0")
        self.index_label.setAlignment(Qt.AlignCenter)
        self.layout.addWidget(self.index_label)

        # 底部布局(按钮和标注信息显示框)
        self.bottom_layout = QHBoxLayout()

        # 标注信息显示框
        self.annotation_display = QPlainTextEdit()
        self.annotation_display.setReadOnly(True)  # 设置为只读
        self.annotation_display.setPlaceholderText("标注信息将显示在这里")
        self.bottom_layout.addWidget(self.annotation_display)

        # 按钮区域
        self.button_layout = QVBoxLayout()
        self.load_folder_button = QPushButton("加载文件夹")
        self.design_button = QPushButton("设计标签")
        self.delete_label_button = QPushButton("删除标签")
        self.save_button = QPushButton("保存标注")
        self.clear_button = QPushButton("清除标注")
        self.button_layout.addWidget(self.annotation_type_combo)
        self.button_layout.addWidget(self.load_folder_button)
        self.button_layout.addWidget(self.design_button)
        self.button_layout.addWidget(self.delete_label_button)
        self.button_layout.addWidget(self.save_button)
        self.button_layout.addWidget(self.clear_button)
        self.bottom_layout.addLayout(self.button_layout)

        self.layout.addLayout(self.bottom_layout)

        # 连接按钮事件
        self.load_folder_button.clicked.connect(self.load_folder)
        self.prev_button.clicked.connect(self.load_prev_image)
        self.next_button.clicked.connect(self.load_next_image)
        self.design_button.clicked.connect(self.design_label_types)
        self.delete_label_button.clicked.connect(self.delete_label_type)
        self.save_button.clicked.connect(self.save_annotations)
        self.clear_button.clicked.connect(self.clear_annotations)

        # 当前图片路径和文件夹信息
        self.current_image_path = None
        self.image_folder = None
        self.image_files = []
        self.current_image_index = -1

        # 标签类型
        self.label_types = []
        self.load_label_types()  # 加载已有的标签类型

    def set_annotation_type(self, annotation_type):
        """设置标注类型"""
        self.image_label.annotation_type = annotation_type

    def load_folder(self):
        """加载文件夹中的所有图片"""
        folder_path = QFileDialog.getExistingDirectory(self, "选择图片文件夹")
        if folder_path:
            self.image_folder = folder_path
            self.image_files = [
                os.path.join(folder_path, f) for f in os.listdir(folder_path)
                if f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp'))
            ]
            if self.image_files:
                self.current_image_index = 0
                self.load_image(self.image_files[self.current_image_index])
                self.update_button_state()
                self.update_index_label()

    def load_image(self, file_path):
        """加载指定路径的图片"""
        if file_path:
            # 使用 OpenCV 读取图像
            image = cv2.imread(file_path)
            if image is not None:
                self.image_label.set_image(image)
                self.current_image_path = file_path  # 保存当前图片路径
                self.image_label.annotations = []  # 清除当前标注
                self.image_label.polygon_points = []  # 清除多边形点
                self.load_annotations()  # 加载已有的标注信息
                self.update_annotation_display()  # 更新标注信息显示

    def load_prev_image(self):
        """加载上一张图片"""
        if self.image_files and self.current_image_index > 0:
            self.current_image_index -= 1
            self.load_image(self.image_files[self.current_image_index])
            self.update_button_state()
            self.update_index_label()

    def load_next_image(self):
        """加载下一张图片"""
        if self.image_files and self.current_image_index < len(self.image_files) - 1:
            self.current_image_index += 1
            self.load_image(self.image_files[self.current_image_index])
            self.update_button_state()
            self.update_index_label()
            self.clear_annotations()

    def update_button_state(self):
        """根据当前图片索引更新按钮状态"""
        self.prev_button.setEnabled(self.current_image_index > 0)
        self.next_button.setEnabled(self.current_image_index < len(self.image_files) - 1)

    def update_index_label(self):
        """更新当前图片索引和总图片数显示"""
        self.index_label.setText(f"{self.current_image_index + 1}/{len(self.image_files)}")

    def design_label_types(self):
        """设计标签类型"""
        label, ok = QInputDialog.getText(self, "添加标签类型", "请输入新的标签类型:")
        if ok and label:
            if label not in self.label_types:
                self.label_types.append(label)
                self.save_label_types()  # 保存标签类型
                QMessageBox.information(self, "成功", f"标签 '{label}' 已添加!")
            else:
                QMessageBox.warning(self, "警告", f"标签 '{label}' 已存在!")

    def delete_label_type(self):
        """删除标签类型"""
        if self.label_types:
            label, ok = QInputDialog.getItem(self, "删除标签类型", "请选择要删除的标签类型:", self.label_types, 0, False)
            if ok and label:
                self.label_types.remove(label)
                self.save_label_types()  # 保存标签类型
                QMessageBox.information(self, "成功", f"标签 '{label}' 已删除!")
        else:
            QMessageBox.warning(self, "警告", "没有可删除的标签类型!")

    def save_label_types(self):
        """保存标签类型到文件"""
        with open("label_types.txt", "w") as f:
            for label in self.label_types:
                f.write(f"{label}\n")

    def load_label_types(self):
        """从文件加载标签类型"""
        if os.path.exists("label_types.txt"):
            with open("label_types.txt", "r") as f:
                self.label_types = [line.strip() for line in f.readlines()]

    def load_annotations(self):
        """加载已有的标注信息"""
        if self.current_image_path is None:
            return

        # 标注文件路径
        image_name = os.path.splitext(os.path.basename(self.current_image_path))[0]
        txt_path = os.path.join(self.image_folder, f"{image_name}_annotations.txt")

        # 清空当前标注
        self.image_label.annotations = []

        # 加载标注信息
        if os.path.exists(txt_path):
            with open(txt_path, "r") as f:
                for line in f:
                    parts = line.strip().split()
                    if len(parts) == 5:
                        label, xmin, ymin, xmax, ymax = parts
                        rect = QRect(int(xmin), int(ymin), int(xmax) - int(xmin), int(ymax) - int(ymin))
                        self.image_label.annotations.append((rect, label))
            self.image_label.update_display()

    def save_annotations(self):
        """保存标注结果"""
        if not self.image_label.annotations:
            QMessageBox.information(self, "信息", "没有标注可保存")
            return

        if self.current_image_path is None:
            QMessageBox.warning(self, "警告", "未加载图片")
            return

        try:
            # 默认保存路径为图片的同路径
            image_name = os.path.splitext(os.path.basename(self.current_image_path))[0]
            txt_path = os.path.join(self.image_folder, f"{image_name}_annotations.txt")

            # 保存标注信息到 TXT 文件
            with open(txt_path, "w") as f:
                for shape, label in self.image_label.annotations:
                    if isinstance(shape, QRect):  # 矩形框
                        f.write(f"{label} {shape.left()} {shape.top()} {shape.right()} {shape.bottom()}\n")
                    elif isinstance(shape, QPolygon):  # 多边形
                        points = ", ".join([f"{point.x()} {point.y()}" for point in shape])
                        f.write(f"{label} {points}\n")
                    elif isinstance(shape, QPoint):  # 点标注
                        f.write(f"{label} {shape.x()} {shape.y()}\n")
            QMessageBox.information(self, "信息", f"标注已保存到: {txt_path}")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"保存标注时出错: {str(e)}")

    def clear_annotations(self):
        """清除所有标注"""
        self.image_label.annotations = []
        self.image_label.update_display()
        self.update_annotation_display()  # 更新标注信息显示

    def update_annotation_display(self):
        """更新标注信息显示框"""
        self.annotation_display.clear()
        for shape, label in self.image_label.annotations:
            if isinstance(shape, QRect):  # 矩形框
                self.annotation_display.appendPlainText(
                    f"{label}: 矩形框 ({shape.left()}, {shape.top()}, {shape.right()}, {shape.bottom()})"
                )
            elif isinstance(shape, QPolygon):  # 多边形
                points = ", ".join([f"({point.x()}, {point.y()})" for point in shape])
                self.annotation_display.appendPlainText(f"{label}: 多边形 [{points}]")
            elif isinstance(shape, QPoint):  # 点标注
                self.annotation_display.appendPlainText(f"{label}: 点 ({shape.x()}, {shape.y()})")

五、源码地址

LabelTool
未来将基于YOLOv8训练一个识别图像内瑕疵的模型。

您可能感兴趣的与本文相关的镜像

Yolo-v5

Yolo-v5

Yolo

YOLO(You Only Look Once)是一种流行的物体检测和图像分割模型,由华盛顿大学的Joseph Redmon 和Ali Farhadi 开发。 YOLO 于2015 年推出,因其高速和高精度而广受欢迎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值