本教程将指导您创建一个专业的图形标注工具,支持以下核心功能:
三点自动闭合算法:
# 当检测到三个点时自动闭合
if len(self.polygon_points) >= 3:
self.polygon_completed.emit(...)
实时预览机制:
数据结构:
{
"polygon": [[x1,y1], [x2,y2], [x3,y3]],
"rectangles": [[x1,y1,x2,y2], ...]
}
保存策略:
{
"polygon": [[100.5, 200.3], [300.2, 150.7], [250.1, 400.9]],
"rectangles": [
[50, 80, 200, 300],
[300, 150, 450, 400]
]
}
"""
GraphMarker - 专业图形标注工具
版本:1.0
作者:蜡笔小新星
"""
import sys
import json
from PyQt5.QtWidgets import (QApplication, QLabel, QVBoxLayout,
QWidget, QMainWindow, QComboBox)
from PyQt5.QtGui import (QPainter, QPen, QCursor, QColor,
QPolygonF, QBrush, QMouseEvent)
from PyQt5.QtCore import Qt, QPointF, QRect, pyqtSignal
class CanvasWidget(QLabel):
"""核心标注画布控件"""
polygon_completed = pyqtSignal(list) # 多边形坐标信号
rectangle_completed = pyqtSignal(list) # 矩形坐标信号
def __init__(self, parent=None):
super().__init__(parent)
self._init_ui()
self._init_drawing_states()
def _init_ui(self):
"""初始化界面参数"""
self.setMouseTracking(True)
self.setFixedSize(800, 600)
self.setStyleSheet("""
background-color: #FFFFFF;
border: 2px solid #999;
qproperty-alignment: AlignCenter;
""")
self.crosshair_color = QColor(255, 0, 0, 150)
self.crosshair_width = 1
def _init_drawing_states(self):
"""初始化绘图状态"""
self.drawing_mode = None
self.polygon_points = []
self.rectangles = []
self.current_rect = None
self.mouse_in_canvas = False
# ---------- 事件处理 ----------
def enterEvent(self, event):
"""鼠标进入画布区域"""
self.mouse_in_canvas = True
self.setCursor(QCursor(Qt.CrossCursor))
self.update()
def leaveEvent(self, event):
"""鼠标离开画布区域"""
self.mouse_in_canvas = False
self.unsetCursor()
self.update()
def mousePressEvent(self, event: QMouseEvent):
"""处理鼠标点击事件"""
if self.drawing_mode == "polygon":
self._handle_polygon_click(event)
elif self.drawing_mode == "rectangle":
self._handle_rect_click(event)
def _handle_polygon_click(self, event):
"""处理多边形绘制点击"""
if event.button() == Qt.LeftButton:
pos = self.mapFromGlobal(event.globalPos())
self.polygon_points.append(QPointF(pos))
self.update()
# 完成多边形时发射信号
if len(self.polygon_points) >= 3:
self.polygon_completed.emit(
[(p.x(), p.y()) for p in self.polygon_points]
)
elif event.button() == Qt.RightButton:
self._reset_polygon()
def _handle_rect_click(self, event):
"""处理矩形绘制点击"""
if event.button() == Qt.LeftButton:
self.current_rect = {
'start': self.mapFromGlobal(event.globalPos()),
'end': None
}
elif event.button() == Qt.RightButton:
self._reset_rectangles()
def mouseMoveEvent(self, event: QMouseEvent):
"""实时更新绘制预览"""
self.update()
def mouseReleaseEvent(self, event: QMouseEvent):
"""完成矩形绘制"""
if self.drawing_mode == "rectangle" and self.current_rect:
end_pos = self.mapFromGlobal(event.globalPos())
rect = QRect(
self.current_rect['start'],
end_pos
).normalized()
self.rectangles.append(rect)
self.rectangle_completed.emit([
(r.x(), r.y(), r.right(), r.bottom())
for r in self.rectangles
])
self.current_rect = None
self.update()
# ---------- 绘图引擎 ----------
def paintEvent(self, event):
"""核心绘图方法"""
super().paintEvent(event)
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing) # 启用抗锯齿
# 绘制辅助准星
if self.mouse_in_canvas:
self._draw_crosshair(painter)
# 按模式绘制图形
if self.drawing_mode == "polygon":
self._draw_polygon(painter)
elif self.drawing_mode == "rectangle":
self._draw_rectangles(painter)
self._draw_rect_preview(painter)
def _draw_crosshair(self, painter: QPainter):
"""绘制十字准星"""
painter.save()
pen = QPen(self.crosshair_color, self.crosshair_width, Qt.DashLine)
painter.setPen(pen)
mouse_pos = self.mapFromGlobal(QCursor.pos())
painter.drawLine(0, mouse_pos.y(), self.width(), mouse_pos.y())
painter.drawLine(mouse_pos.x(), 0, mouse_pos.x(), self.height())
painter.restore()
def _draw_polygon(self, painter: QPainter):
"""绘制多边形系统(稳定版)"""
if not self.polygon_points:
return
painter.save()
# 设置主绘制参数
pen = QPen(QColor(0, 120, 215), 2, Qt.SolidLine)
painter.setPen(pen)
brush = QBrush(QColor(0, 120, 215, 50))
painter.setBrush(brush)
# 绘制闭合多边形
if len(self.polygon_points) >= 3:
polygon = QPolygonF(self.polygon_points)
painter.drawPolygon(polygon)
# 绘制顶点连线(点数不足时)
elif len(self.polygon_points) >= 2:
painter.drawPolyline(QPolygonF(self.polygon_points))
# 绘制顶点标记
painter.setBrush(QBrush(Qt.blue))
for point in self.polygon_points:
painter.drawEllipse(point, 4, 4)
# 绘制实时预览线
if self.polygon_points:
mouse_pos = self.mapFromGlobal(QCursor.pos())
preview_pen = QPen(QColor(0, 120, 215, 150), 2, Qt.DashLine)
painter.setPen(preview_pen)
# 连接最后点到鼠标位置
painter.drawLine(self.polygon_points[-1], QPointF(mouse_pos))
# 三点自动闭合预览
if len(self.polygon_points) >= 2:
painter.drawLine(QPointF(mouse_pos), self.polygon_points[0])
painter.restore()
def _draw_rectangles(self, painter: QPainter):
"""绘制已完成的矩形"""
painter.save()
pen = QPen(QColor(40, 150, 40), 2)
painter.setPen(pen)
for rect in self.rectangles:
painter.drawRect(rect)
painter.restore()
def _draw_rect_preview(self, painter: QPainter):
"""绘制矩形预览"""
if not self.current_rect:
return
painter.save()
pen = QPen(QColor(0, 200, 0, 100), 2, Qt.DashLine)
painter.setPen(pen)
start = self.current_rect['start']
end = self.mapFromGlobal(QCursor.pos())
painter.drawRect(QRect(start, end))
painter.restore()
# ---------- 数据管理 ----------
def set_draw_mode(self, mode: str):
"""设置绘制模式"""
self.drawing_mode = mode
self._reset_all()
self.update()
def _reset_polygon(self):
"""重置多边形数据"""
self.polygon_points.clear()
self.update()
def _reset_rectangles(self):
"""重置矩形数据"""
self.rectangles.clear()
self.current_rect = None
self.update()
def _reset_all(self):
"""重置所有数据"""
self._reset_polygon()
self._reset_rectangles()
class GraphMarker(QMainWindow):
"""主应用程序窗口"""
def __init__(self):
super().__init__()
self._init_ui()
self.data_file = "annotations.json"
def _init_ui(self):
"""初始化用户界面"""
self.setWindowTitle("GraphMarker - 专业图形标注工具")
self.setMinimumSize(1000, 800)
# 创建核心组件
self.canvas = CanvasWidget()
self.tool_box = QComboBox()
self.tool_box.addItems(["选择工具", "多边形标注", "矩形标注"])
# 配置组件样式
self.tool_box.setStyleSheet("""
QComboBox {
padding: 10px;
font: bold 14px '微软雅黑';
min-width: 150px;
border: 2px solid #666;
border-radius: 5px;
}
""")
# 构建布局
layout = QVBoxLayout()
layout.addWidget(self.tool_box, alignment=Qt.AlignCenter)
layout.addWidget(self.canvas, alignment=Qt.AlignCenter)
container = QWidget()
container.setLayout(layout)
self.setCentralWidget(container)
# 连接信号与槽
self.tool_box.currentTextChanged.connect(self._update_tool)
self.canvas.polygon_completed.connect(self._save_polygon)
self.canvas.rectangle_completed.connect(self._save_rectangle)
def _update_tool(self, tool_name):
"""更新当前工具"""
tool_map = {
"选择工具": None,
"多边形标注": "polygon",
"矩形标注": "rectangle"
}
self.canvas.set_draw_mode(tool_map[tool_name])
def _save_polygon(self, points):
"""保存多边形数据(覆盖模式)"""
data = self._load_data()
data["polygon"] = points # 覆盖原有数据
self._save_data(data)
def _save_rectangle(self, rects):
"""保存矩形数据(覆盖模式)"""
data = self._load_data()
data["rectangles"] = rects # 覆盖原有数据
self._save_data(data)
def _load_data(self):
"""加载存储数据"""
try:
with open(self.data_file, 'r') as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return {"polygon": [], "rectangles": []}
def _save_data(self, data):
"""执行数据持久化"""
try:
with open(self.data_file, 'w') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
print("标注数据已安全保存")
except Exception as e:
print(f"保存失败:{str(e)}")
if __name__ == "__main__":
app = QApplication(sys.argv)
window = GraphMarker()
window.show()
sys.exit(app.exec_())
标注编辑功能:
可视化增强:
导入/导出:
本教程代码经过严格测试,支持Windows/macOS/Linux系统,可直接用于生产环境。读者可根据实际需求在此基础框架上进行功能扩展,构建专业的图形标注系统。