记录第一次pyqt的项目制作

文章详细描述了一个使用Python和PyQt6开发的图像标注工具,通过CustomPixmapItem类实现了图片导入、贝塞尔曲线标记,并能保存标记点数据。开发者计划进一步学习PyQt和C++以优化功能。
摘要由CSDN通过智能技术生成

首先设计了主窗口,这里窗口的布局大致为

根据老板的需求,需要导入图片,然后标记图片,标记的方式,是用贝塞尔曲线的方式,这里做一个记录,其中部分代码还是搬运的,反正实现了大致的功能,可以根据贝塞尔曲线得到采样的等分点,然后后续可以进行相关的操作。

from __future__ import annotations

import math
import sys
import os
from PyQt6.QtWidgets import (QApplication, QMainWindow, QPushButton, QGraphicsView, QGraphicsScene,
                             QHBoxLayout, QVBoxLayout, QWidget, QFileDialog, QListWidget, QGraphicsPixmapItem,
                             QLineEdit, QMessageBox)
from PyQt6.QtGui import QPixmap, QMouseEvent, QPen, QPainterPath, QPaintEvent, QPainter, QFontMetrics, QImage, QColor
from PyQt6.QtCore import Qt, QPointF
import json
from PyQt6.QtGui import QPainter, QPen, QFontMetrics
class CustomPixmapItem(QGraphicsPixmapItem):
    def __init__(self, pixmap):
        super().__init__(pixmap)
        self.points = []
        self.pos = []
        self.current_point_index = -1
        self.bezierPrecision=0.001

    def mousePressEvent(self, event):
        # 直接使用 event.pos() 获取鼠标点击的位置
        scenePos = self.mapToScene(event.pos())

        # 获取场景中的所有项
        items = self.scene().items(scenePos)
        if items and isinstance(items[0], QGraphicsPixmapItem):
            item = items[0]
            imagePos = item.mapFromScene(scenePos)
            # 检查坐标是否在图像内部
            if imagePos.x() >= 0 and imagePos.y() >= 0 and imagePos.x() < item.pixmap().width() and imagePos.y() < item.pixmap().height():
                if event.button() == Qt.MouseButton.RightButton:
                    self.points.append(imagePos)
                    self.update()
                    print(self.points)

                elif event.button() == Qt.MouseButton.LeftButton:
                    for i, p in enumerate(self.points):
                        if (imagePos - p).manhattanLength() < 30:
                            self.current_point_index = i
                            break

    def mouseMoveEvent(self, event):
        if event.buttons() == Qt.MouseButton.LeftButton and self.current_point_index != -1:
            # 获取鼠标当前位置的场景坐标
            scenePos = self.mapToScene(event.pos())

            # 获取场景中的所有项
            items = self.scene().items(scenePos)

            # 检查是否有项被点击,并且该项是图像项
            if items and isinstance(items[0], QGraphicsPixmapItem):
                item = items[0]

                # 将场景坐标转换为图像坐标
                imagePos = item.mapFromScene(scenePos)

                # 检查坐标是否在图像内部
                if imagePos.x() >= 0 and imagePos.y() >= 0 and imagePos.x() < item.pixmap().width() and imagePos.y() < item.pixmap().height():
                    # 更新当前控制点的位置
                    self.points[self.current_point_index] = imagePos
                    self.update()

    def mouseReleaseEvent(self, event):
        self.current_point_index = -1

    def loadPoints(self, points):
        self.points = [QPointF(x, y) for x, y in points]
        self.update()

    def paint(self, painter, option, widget=None):
        # 首先调用父类的paint方法来绘制图像本身
        super().paint(painter, option, widget)

        # 设置抗锯齿
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)

        # 绘制点和曲线
        self.drawPoint(painter)
        self.drawBezierCurve(painter)

    def drawPoint(self, painter):
        pen = QPen(Qt.GlobalColor.red, 15)
        pen.setCapStyle(Qt.PenCapStyle.RoundCap)
        painter.setPen(pen)
        for i, p in enumerate(self.points):
            painter.drawPoint(p)
            # 绘制索引
            painter.setPen(Qt.GlobalColor.red)
            rect = QFontMetrics(painter.font()).boundingRect(str(i))
            painter.drawText(p + QPointF(-rect.width() / 2, rect.height() / 2 - 2.0), str(i))

    def drawBezierCurve(self, painter):
        # 贝塞尔曲线
        n = len(self.points) - 1
        if n <= 0:
            return
        pos = []
        step = self.bezierPrecision  # 精度 等分数
        t = 0
        while t <= 1:
            b = []
            for j in range(n + 1):
                b.append(math.comb(n, j) * pow(t, j) * pow(1 - t, n - j))
            x = y = 0
            for j in range(n + 1):
                x += self.points[j].x() * b[j]
                y += self.points[j].y() * b[j]
            pos.append(QPointF(x, y))
            t += step
        pos.append(QPointF(self.points[-1]))

        # 开始绘制
        # 创建一个蓝色的QColor对象,透明度设置为128(半透明)
        transparentBlue = QColor(0, 0, 255, 50)

        painter.setPen(QPen(transparentBlue, 3))
        path = QPainterPath()
        path.moveTo(pos[0])
        for i in range(len(pos) - 1):
            path.lineTo(pos[i + 1])
        painter.drawPath(path)
        self.pos=pos

    def saveBezierCurvePoints(self, filename):
        # 按照X坐标对曲线上的点进行排序
        sorted_points = sorted(self.pos, key=lambda p: p.x())
        sorted_points_control=sorted(self.points,key=lambda p: p.x())
        # 创建一个新的空白图片(尺寸与原图相同,背景为白色)
        original_pixmap = self.pixmap()
        new_image = QImage(original_pixmap.size(), QImage.Format.Format_RGB32)
        new_image.fill(Qt.GlobalColor.white)

        # 在新图片上绘制贝塞尔曲线
        painter = QPainter(new_image)
        painter.setPen(QPen(QColor(0, 0, 0), 3))  # 黑色画笔
        path = QPainterPath()
        path.moveTo(sorted_points[0])
        for p in sorted_points[1:]:
            path.lineTo(p)
        painter.drawPath(path)
        painter.end()

        # 保存新图片
        new_image.save(filename.replace('.txt', '.png'))

        # 可选:将点的坐标写入文件
        with open(filename, 'w') as file:
            for p in sorted_points:
                file.write(f"{p.x()} {p.y()}\n")
        with open(f"{filename.split('.')[0]}_control.txt",'w') as file:
            for p in sorted_points_control:
                file.write(f"{p.x()} {p.y()}\n")

    # 在适当的地方调用这个方法,例如在绘制曲线之后
    # 例如:self.saveBezierCurvePoints('bezier_curve_points.txt')


class GraphicsView(QGraphicsView):
    def __init__(self, scene, parent=None):
        super().__init__(scene, parent)
        self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
        self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
        # 启用滚动条
        self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
        self.bezierPrecision=-1

    def wheelEvent(self, event):
        # 检查是否按下了Ctrl键
        if event.modifiers() & Qt.KeyboardModifier.ControlModifier:
            # 获取鼠标滚轮事件的位置(视图坐标)
            viewPos = event.position()

            # 将视图坐标转换为场景坐标
            scenePos = self.mapToScene(viewPos.toPoint())
            # 设置放大或缩小的中心为鼠标所在位置
            self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
            self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)

            scaleFactor = 1.15
            # 向前滚动放大,向后滚动缩小
            if event.angleDelta().y() > 0:
                self.scale(scaleFactor, scaleFactor)
            else:
                self.scale(1 / scaleFactor, 1 / scaleFactor)
            self.centerOn(scenePos)
        else:
            # 没有按Ctrl键,正常处理滚动事件
            super().wheelEvent(event)

    def updateBezierPrecision(self, precision):
        self.bezierPrecision = precision
        self.scene().update()  # 重新绘制场景


class ImageViewer(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('Image Viewer')
        self.setGeometry(100, 100, 800, 600)
        self.folder_path = ''
        self.file_name=""
        self.total_path=''
        self.annotationFile = 'annotations.json'  # JSON 文件名
        self.loadAnnotations()  # 加载标注信息
        self.precisionInput_number=0
        self.processedImages = {}  # 新增字典来存储处理过的图片及其标注
        # 创建一个列表以显示图片文件名
        self.file_list = QListWidget()
        self.file_list.setMaximumWidth(200)
        self.item=None
        # 创建图形视图和场景以显示图片
        self.scene = QGraphicsScene()
        # 修改这一行,使用自定义的 GraphicsView
        self.graphics_view = GraphicsView(self.scene)
        # 创建输入框和按钮让用户设置精度
        self.precisionInput = QLineEdit()
        self.precisionInput.setMaximumWidth(200)
        self.precisionInput.setPlaceholderText("must be divisible by 1")
        self.setPrecisionButton = QPushButton("Set aliquot")
        self.setPrecisionButton.clicked.connect(self.setBezierPrecision)
        self.clear_button=QPushButton("Clear")
        # 创建Open和Save按钮
        self.open_button = QPushButton('Open')
        self.open_button.clicked.connect(self.open_folder)
        self.save_button = QPushButton('Save')
        self.save_button.clicked.connect(self.savePoints)
        self.clear_button.clicked.connect(self.Clear_point)
        # self.save_button.clicked.connect(self.save_image) # 如果需要保存功能的话

        # 创建一个垂直布局,用于左侧的文件列表和按钮
        left_layout = QVBoxLayout()
        left_layout.addWidget(self.open_button)
        left_layout.addWidget(self.file_list)
        left_layout.addWidget(self.save_button)
        left_layout.addWidget(self.clear_button)
        left_layout.addWidget(self.precisionInput)
        left_layout.addWidget(self.setPrecisionButton)

        # 创建一个水平布局,用于整个窗口
        main_layout = QHBoxLayout()
        main_layout.addLayout(left_layout)  # 添加左侧布局
        main_layout.addWidget(self.graphics_view)  # 添加图像显示

        # 创建一个widget作为主要布局,并将其设置为中心widget
        central_widget = QWidget()
        central_widget.setLayout(main_layout)
        self.setCentralWidget(central_widget)

        # 连接列表的选中信号到一个函数
        self.file_list.currentTextChanged.connect(self.display_image)

    def loadAnnotations(self):
        # 检查文件是否存在
        if os.path.exists(self.annotationFile):
            try:
                with open(self.annotationFile, 'r') as file:
                    self.processedImages = json.load(file)
                print("Loaded annotations:", self.processedImages)
            except json.JSONDecodeError as e:
                print(f"Error decoding JSON from {self.annotationFile}: {e}")
                self.processedImages = {}
        else:
            print(f"No annotation file found at {self.annotationFile}")
            self.processedImages = {}

    def saveAnnotations(self):
        """保存标注信息到 JSON 文件"""

        with open(self.annotationFile, 'w') as file:
            print(self.processedImages)
            json.dump(self.processedImages, file, indent=4)

    def Clear_point(self):
        if(self.item!=None):
            self.item.points=[]
            self.processedImages[self.total_path] = []
            QMessageBox.warning(self, "Right", "清除完成")

    def setBezierPrecision(self):
        if(self.graphics_view.bezierPrecision!=-1):
            QMessageBox.warning(self, "Error", "均分值已经确定,不能再改变")
            self.precisionInput.setText(str(self.precisionInput_number))
        else:
            try:
                precisionValue = int(self.precisionInput.text())

                if precisionValue > 0:  # 确保输入是正数
                    self.graphics_view.updateBezierPrecision(1 / precisionValue)
                    self.precisionInput_number = precisionValue
                    QMessageBox.warning(self, "Right", "均分值设置成功")
                else:
                    QMessageBox.warning(self, "Error", "请输入正整数")
            except ValueError:
                QMessageBox.warning(self, "Error", "请输入正整数")

    def open_folder(self):
        if(self.precisionInput.text() == "" or self.graphics_view.bezierPrecision==-1):
            QMessageBox.warning(self, "Error", "请输入均分数量,并点击Set aliquot")
        else:
            folder_path = QFileDialog.getExistingDirectory(self, 'Select Folder')
            if folder_path:
                self.folder_path = folder_path
                self.load_images_from_folder(folder_path)

    def load_images_from_folder(self, folder_path):
        self.file_list.clear()
        for filename in os.listdir(folder_path):
            if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')):
                self.file_list.addItem(filename)


    def display_image(self, filename):
        if filename:
            path=os.path.join(self.folder_path, filename)
            self.total_path=path
            pixmap = QPixmap(path)
            print(path)
            self.item = CustomPixmapItem(pixmap)
            self.scene.clear()
            self.scene.addItem(self.item)
            self.graphics_view.fitInView(self.item, Qt.AspectRatioMode.KeepAspectRatio)
            self.file_name=filename
            self.item.bezierPrecision=self.graphics_view.bezierPrecision
            if not (os.path.exists(fr"{self.folder_path}\Bmodule")):
                os.mkdir(fr"{self.folder_path}\Bmodule")
            self.annotationFile=fr"{self.folder_path}\Bmodule\{self.file_name.split('.')[0]}.json"
            print(self.annotationFile)
            self.loadAnnotations()
            # 检查图片是否已经处理过并加载标注
            if path in self.processedImages:
                self.item.loadPoints(self.processedImages[path])
                QMessageBox.information(self, "Information", "之前标记过的标记点已经自动加载")

    def savePoints(self):
        # 检查是否有图像项并且其中有曲线点
        if self.scene.items() and isinstance(self.scene.items()[0], CustomPixmapItem):
            item = self.scene.items()[0]
            if item.pos:
                # 指定保存文件的路径和名称
                filename = fr"{self.folder_path}\Bmodule\{self.file_name.split('.')[0]}.txt"
                print(filename)
                item.saveBezierCurvePoints(filename)
                print(f"Points saved to {filename}")
                points_to_save = [[point.x(), point.y()] for point in self.item.points]
                self.processedImages[self.total_path] = points_to_save
                self.saveAnnotations()  # 保存标注信息到 JSON 文件
                QMessageBox.warning(self, "Right", "成功保存")


if __name__ == '__main__':
    app = QApplication(sys.argv)
    viewer = ImageViewer()
    viewer.show()
    sys.exit(app.exec())

后续打算继续学习pyqt和C++,然后希望能够根据C++的qt方式去实现这个功能。

就当记录了。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值