首先设计了主窗口,这里窗口的布局大致为
根据老板的需求,需要导入图片,然后标记图片,标记的方式,是用贝塞尔曲线的方式,这里做一个记录,其中部分代码还是搬运的,反正实现了大致的功能,可以根据贝塞尔曲线得到采样的等分点,然后后续可以进行相关的操作。
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方式去实现这个功能。
就当记录了。