import sys
import os
import math
import numpy as np
import moderngl as mg
from PyQt5.QtWidgets import (QApplication, QMainWindow, QGraphicsView,
QGraphicsScene, QGraphicsPathItem,
QGraphicsLineItem, QVBoxLayout, QWidget,
QFileDialog, QHBoxLayout, QPushButton,
QLabel, QMessageBox, QInputDialog, QSpinBox,
QGraphicsItemGroup, QDialog, QComboBox,
QCheckBox, QSlider, QGroupBox,
QFormLayout, QDoubleSpinBox, QSizePolicy, QOpenGLWidget)
from PyQt5.QtCore import Qt, QPointF, QRectF, QPoint, QLineF, pyqtSignal
from PyQt5.QtGui import (QPen, QColor, QFont, QPixmap, QPainter, QBrush,
QPainterPath, QImage, QTransform, QFontMetrics,
QPalette, QSurfaceFormat)
from PyQt5.QtPrintSupport import QPrinter
# 使用ModernGL实现OpenGL渲染
class ModernGLWidget(QOpenGLWidget):
def __init__(self, parent=None):
fmt = QSurfaceFormat()
fmt.setVersion(3, 3)
fmt.setProfile(QSurfaceFormat.CoreProfile)
fmt.setSamples(8)
fmt.setSwapBehavior(QSurfaceFormat.DoubleBuffer)
fmt.setDepthBufferSize(24)
fmt.setStencilBufferSize(8)
fmt.setOption(QSurfaceFormat.DebugContext)
QSurfaceFormat.setDefaultFormat(fmt)
super().__init__(parent)
self.ctx = None
self.program = None
self.texture = None
self.image = None
self.image_size = (0, 0)
self.vertex_buffer = None
self.index_buffer = None
self.vao = None
self.texture_created = False
def initializeGL(self):
try:
# 初始化ModernGL上下文
self.ctx = mg.create_context(require=330)
# 确保上下文创建成功
if not self.ctx:
raise Exception("无法创建ModernGL上下文")
# 创建着色器程序 - 确保#version是代码的第一行
vertex_shader_code = (
"#version 330\n"
"in vec2 in_vert;\n"
"in vec2 in_texcoord;\n"
"out vec2 v_texcoord;\n"
"void main() {\n"
" gl_Position = vec4(in_vert, 0.0, 1.0);\n"
" v_texcoord = in_texcoord;\n"
"}\n"
)
fragment_shader_code = (
"#version 330\n"
"uniform sampler2D tex;\n"
"in vec2 v_texcoord;\n"
"out vec4 f_color;\n"
"void main() {\n"
" f_color = texture(tex, v_texcoord);\n"
"}\n"
)
# 打印着色器代码以便调试
print("顶点着色器代码:")
print(vertex_shader_code)
print("片段着色器代码:")
print(fragment_shader_code)
self.program = self.ctx.program(
vertex_shader=vertex_shader_code,
fragment_shader=fragment_shader_code
)
# 创建全屏四边形
vertices = np.array([
# x, y, u, v
-1.0, -1.0, 0.0, 0.0,
1.0, -1.0, 1.0, 0.0,
-1.0, 1.0, 0.0, 1.0,
1.0, 1.0, 1.0, 1.0,
], dtype='f4')
indices = np.array([0, 1, 2, 1, 3, 2], dtype='i4')
self.vertex_buffer = self.ctx.buffer(vertices.tobytes())
self.index_buffer = self.ctx.buffer(indices.tobytes())
self.vao = self.ctx.vertex_array(
self.program,
[
(self.vertex_buffer, '2f 2f', 'in_vert', 'in_texcoord')
],
self.index_buffer
)
# 创建空纹理
self.texture = self.ctx.texture((1, 1), 4)
self.texture.use(0)
self.texture_created = True
print("ModernGL上下文初始化成功")
print(f"OpenGL版本: {self.ctx.version_code}")
print(f"供应商: {self.ctx.info['GL_VENDOR']}")
print(f"渲染器: {self.ctx.info['GL_RENDERER']}")
except Exception as e:
print(f"ModernGL初始化失败: {e}")
self.ctx = None
self.texture_created = False
def set_image(self, image):
"""设置要显示的图像"""
if not self.texture_created:
return
if image is None or image.isNull():
return
self.image = image
self.image_size = (image.width(), image.height())
# 将QImage转换为OpenGL纹理
if self.ctx is None:
return
# 转换为RGBA格式
if image.format() != QImage.Format_RGBA8888:
image = image.convertToFormat(QImage.Format_RGBA8888)
# 获取图像数据
ptr = image.constBits()
if ptr is None:
print("无法获取图像数据指针")
return
ptr.setsize(image.byteCount())
data = np.frombuffer(ptr, dtype=np.uint8).reshape(image.height(), image.width(), 4)
try:
# 创建或更新纹理
if self.texture is None:
self.texture = self.ctx.texture((image.width(), image.height()), 4, data.tobytes())
self.texture.use(0)
elif self.texture.size != (image.width(), image.height()):
self.texture.release()
self.texture = self.ctx.texture((image.width(), image.height()), 4, data.tobytes())
self.texture.use(0)
else:
self.texture.write(data.tobytes())
self.update()
except Exception as e:
print(f"设置纹理时出错: {e}")
def paintGL(self):
"""渲染场景"""
if self.ctx is None or not self.texture_created:
return
try:
# 清除缓冲区
self.ctx.clear(0.2, 0.2, 0.2, 1.0)
# 渲染图像
if self.texture:
self.vao.render()
# 确保交换缓冲区
self.ctx.finish()
except Exception as e:
print(f"渲染时出错: {e}")
def resizeGL(self, width, height):
"""处理窗口大小变化"""
if self.ctx:
try:
self.ctx.viewport = (0, 0, width, height)
except Exception as e:
print(f"调整视口大小时出错: {e}")
# 使用逻辑单位定义线条宽度和字体大小(毫米)
LOGICAL_LINE_WIDTH = 0.5 # 毫米
LOGICAL_FONT_SIZE = 3.0 # 毫米
SCREEN_DPI = 96.0 # 假设屏幕DPI为96
HIGH_RES_DPI = 600 # 高分辨率PDF输出DPI
class ImageProcessingDialog(QDialog):
apply_changes = pyqtSignal(int, int, float, bool)
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("图像处理设置")
self.setGeometry(300, 300, 400, 300)
layout = QVBoxLayout()
self.setLayout(layout)
# 亮度调整
brightness_layout = QHBoxLayout()
brightness_layout.addWidget(QLabel("亮度:"))
self.brightness_slider = QSlider(Qt.Horizontal)
self.brightness_slider.setRange(-100, 100)
self.brightness_slider.setValue(0)
brightness_layout.addWidget(self.brightness_slider)
self.brightness_value = QLabel("0")
brightness_layout.addWidget(self.brightness_value)
layout.addLayout(brightness_layout)
# 对比度调整
contrast_layout = QHBoxLayout()
contrast_layout.addWidget(QLabel("对比度:"))
self.contrast_slider = QSlider(Qt.Horizontal)
self.contrast_slider.setRange(-100, 100)
self.contrast_slider.setValue(0)
contrast_layout.addWidget(self.contrast_slider)
self.contrast_value = QLabel("0")
contrast_layout.addWidget(self.contrast_value)
layout.addLayout(contrast_layout)
# Gamma调整
gamma_layout = QHBoxLayout()
gamma_layout.addWidget(QLabel("Gamma校正:"))
self.gamma_slider = QSlider(Qt.Horizontal)
self.gamma_slider.setRange(10, 300) # 0.1 to 3.0
self.gamma_slider.setValue(100) # 1.0
gamma_layout.addWidget(self.gamma_slider)
self.gamma_value = QLabel("1.0")
gamma_layout.addWidget(self.gamma_value)
layout.addLayout(gamma_layout)
# 锐化开关
self.sharpen_check = QCheckBox("锐化图像")
layout.addWidget(self.sharpen_check)
# 连接信号
self.brightness_slider.valueChanged.connect(
lambda v: self.brightness_value.setText(str(v)))
self.contrast_slider.valueChanged.connect(
lambda v: self.contrast_value.setText(str(v)))
self.gamma_slider.valueChanged.connect(
lambda v: self.gamma_value.setText(f"{v/100:.1f}"))
# 按钮
btn_layout = QHBoxLayout()
self.apply_btn = QPushButton("应用")
self.apply_btn.clicked.connect(self.apply)
btn_layout.addWidget(self.apply_btn)
self.reset_btn = QPushButton("重置")
self.reset_btn.clicked.connect(self.reset)
btn_layout.addWidget(self.reset_btn)
self.cancel_btn = QPushButton("取消")
self.cancel_btn.clicked.connect(self.close)
btn_layout.addWidget(self.cancel_btn)
layout.addLayout(btn_layout)
def set_values(self, brightness, contrast, gamma, sharpen):
self.brightness_slider.setValue(brightness)
self.contrast_slider.setValue(contrast)
self.gamma_slider.setValue(int(gamma * 100))
self.sharpen_check.setChecked(sharpen)
def apply(self):
brightness = self.brightness_slider.value()
contrast = self.contrast_slider.value()
gamma = self.gamma_slider.value() / 100.0
sharpen = self.sharpen_check.isChecked()
self.apply_changes.emit(brightness, contrast, gamma, sharpen)
self.close()
def reset(self):
self.brightness_slider.setValue(0)
self.contrast_slider.setValue(0)
self.gamma_slider.setValue(100)
self.sharpen_check.setChecked(False)
class PDFGraphicsView(QGraphicsView):
def __init__(self, scene, gl_widget=None):
super().__init__(scene)
if gl_widget:
self.setViewport(gl_widget)
# 设置高质量的缩放策略
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
self.setRenderHints(
QPainter.Antialiasing |
QPainter.TextAntialiasing |
QPainter.SmoothPixmapTransform
)
# 初始化变量
self.zoom_level = 0
self.max_zoom = 10
self.min_zoom = -10
def wheelEvent(self, event):
if self.parent() and hasattr(self.parent(), 'on_wheel_event'):
self.parent().on_wheel_event(event)
else:
# 优化缩放行为
factor = 1.1 if event.angleDelta().y() > 0 else 1/1.1
new_zoom = self.zoom_level + (1 if event.angleDelta().y() > 0 else -1)
if new_zoom <= self.max_zoom and new_zoom >= self.min_zoom:
self.zoom_level = new_zoom
self.scale(factor, factor)
def mousePressEvent(self, event):
if self.parent() and hasattr(self.parent(), 'on_mouse_press'):
self.parent().on_mouse_press(event)
else:
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.parent() and hasattr(self.parent(), 'on_mouse_move'):
self.parent().on_mouse_move(event)
else:
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if self.parent() and hasattr(self.parent(), 'on_mouse_release'):
self.parent().on_mouse_release(event)
else:
super().mouseReleaseEvent(event)
class PDFAnnotationTool(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("PDF标注工具 (ModernGL硬件加速版)")
self.setGeometry(100, 100, 1200, 900)
# 初始化变量
self.last_rect = None
self.circle_count = 1
self.sub_count = 1
self.current_rect = None
self.points = []
self.is_annotating = False
self.annotation_mode = None
self.is_deleting_single = False
self.annotation_groups = []
self.last_mouse_pos = None
self.is_panning = False
self.background_pixmap = None
self.background_item = None
self.original_image = None
self.display_image = None
self.image_scale = 1.0
self.use_opengl = True
self.render_quality = 1 # 1=快速, 2=平衡, 3=高质量
self.image_processing = {
'brightness': 0,
'contrast': 0,
'gamma': 1.0,
'sharpen': False
}
# 使用逻辑单位
self.logical_line_width = LOGICAL_LINE_WIDTH
self.logical_font_size = LOGICAL_FONT_SIZE
self.screen_dpi = SCREEN_DPI
self.high_res_dpi = HIGH_RES_DPI
# 创建主界面
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout()
central_widget.setLayout(main_layout)
# 创建控制面板
control_panel = QWidget()
control_layout = QHBoxLayout()
control_panel.setLayout(control_layout)
# 添加控制按钮
self.open_btn = QPushButton("打开图像")
self.open_btn.clicked.connect(self.open_image)
control_layout.addWidget(self.open_btn)
self.save_btn = QPushButton("保存为PDF")
self.save_btn.clicked.connect(self.save_as_pdf)
self.save_btn.setEnabled(False)
control_layout.addWidget(self.save_btn)
self.continuous_btn = QPushButton("连续标注")
self.continuous_btn.clicked.connect(lambda: self.start_annotation('continuous'))
self.continuous_btn.setEnabled(False)
control_layout.addWidget(self.continuous_btn)
self.range_btn = QPushButton("范围标注")
self.range_btn.clicked.connect(lambda: self.start_annotation('range'))
self.range_btn.setEnabled(False)
control_layout.addWidget(self.range_btn)
self.clear_all_btn = QPushButton("清除所有标注")
self.clear_all_btn.clicked.connect(self.clear_all_annotations)
self.clear_all_btn.setEnabled(False)
control_layout.addWidget(self.clear_all_btn)
self.clear_single_btn = QPushButton("清除单个标注")
self.clear_single_btn.clicked.connect(self.start_single_deletion)
self.clear_single_btn.setEnabled(False)
control_layout.addWidget(self.clear_single_btn)
# 字体大小设置(显示用,实际使用逻辑单位)
control_layout.addWidget(QLabel("字体大小(mm):"))
self.font_size_spin = QSpinBox()
self.font_size_spin.setRange(1, 100)
self.font_size_spin.setValue(int(self.logical_font_size))
self.font_size_spin.valueChanged.connect(self.set_font_size)
control_layout.addWidget(self.font_size_spin)
# 添加DPI设置控件
control_layout.addWidget(QLabel("PDF分辨率(DPI):"))
self.dpi_spin = QSpinBox()
self.dpi_spin.setRange(72, 2400)
self.dpi_spin.setValue(self.high_res_dpi)
self.dpi_spin.valueChanged.connect(self.set_output_dpi)
control_layout.addWidget(self.dpi_spin)
# 添加渲染质量设置
control_layout.addWidget(QLabel("渲染质量:"))
self.quality_combo = QComboBox()
self.quality_combo.addItems(["快速", "平衡", "高质量"])
self.quality_combo.setCurrentIndex(2) # 默认高质量
self.quality_combo.currentIndexChanged.connect(self.set_render_quality)
control_layout.addWidget(self.quality_combo)
# 添加OpenGL开关
self.opengl_check = QCheckBox("启用ModernGL加速")
self.opengl_check.setChecked(True)
self.opengl_check.stateChanged.connect(self.toggle_opengl)
control_layout.addWidget(self.opengl_check)
# 添加图像处理选项
self.process_btn = QPushButton("图像处理...")
self.process_btn.clicked.connect(self.show_image_processing_dialog)
self.process_btn.setEnabled(False)
control_layout.addWidget(self.process_btn)
self.status_label = QLabel("请打开一个图像文件")
control_layout.addWidget(self.status_label)
control_layout.addStretch()
main_layout.addWidget(control_panel)
# 创建图形视图和ModernGL部件
self.scene = QGraphicsScene()
self.scene.setBackgroundBrush(QBrush(QColor(60, 60, 60)))
# 创建ModernGL部件
try:
self.gl_widget = ModernGLWidget()
self.view = PDFGraphicsView(self.scene, self.gl_widget)
self.use_opengl = True
print("ModernGL加速已启用")
except Exception as e:
print(f"无法初始化ModernGL: {e}, 使用标准视图")
self.view = PDFGraphicsView(self.scene)
self.use_opengl = False
self.opengl_check.setChecked(False)
self.gl_widget = None
# 设置高质量渲染提示
self.view.setRenderHint(QPainter.Antialiasing, True)
self.view.setRenderHint(QPainter.TextAntialiasing, True)
self.view.setDragMode(QGraphicsView.ScrollHandDrag)
self.view.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)
self.view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
main_layout.addWidget(self.view)
# 连接鼠标事件
self.view.mousePressEvent = self.on_mouse_press
self.view.mouseMoveEvent = self.on_mouse_move
self.view.mouseReleaseEvent = self.on_mouse_release
self.view.wheelEvent = self.on_wheel_event
# 初始化图像处理对话框
self.image_processing_dialog = None
def set_font_size(self, size):
self.logical_font_size = size
def set_output_dpi(self, dpi):
self.high_res_dpi = dpi
def set_render_quality(self, index):
self.render_quality = index + 1
self.update_image_display()
def toggle_opengl(self, state):
self.use_opengl = (state == Qt.Checked)
if self.gl_widget:
self.gl_widget.setVisible(self.use_opengl)
self.update_image_display()
def show_image_processing_dialog(self):
if not self.original_image:
return
if not self.image_processing_dialog:
self.image_processing_dialog = ImageProcessingDialog(self)
self.image_processing_dialog.apply_changes.connect(self.apply_image_processing)
self.image_processing_dialog.set_values(
self.image_processing['brightness'],
self.image_processing['contrast'],
self.image_processing['gamma'],
self.image_processing['sharpen']
)
self.image_processing_dialog.show()
def apply_image_processing(self, brightness, contrast, gamma, sharpen):
self.image_processing = {
'brightness': brightness,
'contrast': contrast,
'gamma': gamma,
'sharpen': sharpen
}
self.update_image_display()
def update_image_display(self):
if not self.original_image:
return
# 应用图像处理
img = self.process_image(self.original_image.copy())
# 更新ModernGL部件
if self.use_opengl and self.gl_widget:
self.gl_widget.set_image(img)
# 更新场景背景
if not self.use_opengl:
if self.background_item:
self.scene.removeItem(self.background_item)
self.display_image = QPixmap.fromImage(img)
self.background_item = self.scene.addPixmap(self.display_image)
self.background_item.setTransformationMode(Qt.SmoothTransformation)
self.scene.setSceneRect(0, 0, self.display_image.width(), self.display_image.height())
# 刷新视图
self.view.viewport().update()
def process_image(self, img):
"""应用图像处理效果"""
# 转换为32位ARGB格式进行处理
img = img.convertToFormat(QImage.Format_ARGB32)
# 获取图像数据
width = img.width()
height = img.height()
ptr = img.bits()
if ptr is None:
print("无法获取图像数据指针")
return img
ptr.setsize(img.byteCount())
arr = np.frombuffer(ptr, dtype=np.uint8).reshape(height, width, 4) # 高度, 宽度, 4通道
# 应用亮度调整
brightness = self.image_processing['brightness']
if brightness != 0:
arr = np.clip(arr.astype(np.int32) + brightness, 0, 255).astype(np.uint8)
# 应用对比度调整
contrast = self.image_processing['contrast']
if contrast != 0:
factor = (259 * (contrast + 255)) / (255 * (259 - contrast))
arr = np.clip(factor * (arr.ast(np.int32) - 128) + 128, 0, 255).astype(np.uint8)
# 应用gamma校正
gamma = self.image_processing['gamma']
if gamma != 1.0:
# 归一化到0-1范围
normalized = arr / 255.0
# 应用gamma校正
corrected = np.power(normalized, gamma)
# 缩放回0-255范围
arr = (corrected * 255).astype(np.uint8)
# 应用锐化
if self.image_processing['sharpen']:
# 创建锐化核
kernel = np.array([[-1, -1, -1],
[-1, 9, -1],
[-1, -1, -1]])
# 对每个通道分别应用卷积
for c in range(3): # RGB通道
channel = arr[:, :, c].copy()
# 创建输出数组
output = np.zeros_like(channel)
# 手动应用卷积
for i in range(1, height-1):
for j in range(1, width-1):
region = channel[i-1:i+2, j-1:j+2]
output[i, j] = np.clip(np.sum(region * kernel), 0, 255)
arr[:, :, c] = output
# 创建新的QImage
processed_img = QImage(arr.data, width, height, img.bytesPerLine(), QImage.Format_ARGB32)
# 需要复制数据,否则数组被回收后数据无效
return processed_img.copy()
def open_image(self):
options = QFileDialog.Options()
file_path, _ = QFileDialog.getOpenFileName(
self, "选择图像文件", "",
"Image Files (*.png *.jpg *.jpeg *.bmp *.tif *.tiff);;All Files (*)",
options=options
)
if file_path:
try:
self.scene.clear()
self.last_rect = None
self.circle_count = 1
self.sub_count = 1
self.is_annotating = False
self.is_deleting_single = False
self.annotation_mode = None
self.annotation_groups = []
# 使用QImage加载原始图像
self.original_image = QImage(file_path)
if self.original_image.isNull():
raise Exception("无法加载图像文件")
# 重置图像处理参数
self.image_processing = {
'brightness': 0,
'contrast': 0,
'gamma': 1.0,
'sharpen': False
}
# 创建显示用的图像
self.update_image_display()
# 创建用于PDF输出的高质量图像
self.background_pixmap = QPixmap.fromImage(self.original_image)
self.status_label.setText(f"已加载: {os.path.basename(file_path)}")
self.clear_all_btn.setEnabled(True)
self.clear_single_btn.setEnabled(True)
self.continuous_btn.setEnabled(True)
self.range_btn.setEnabled(True)
self.save_btn.setEnabled(True)
self.process_btn.setEnabled(True)
# 调整视图大小
self.view.fitInView(self.scene.itemsBoundingRect(), Qt.KeepAspectRatio)
except Exception as e:
self.show_error(f"无法加载图像: {str(e)}")
self.status_label.setText("图像加载失败")
def save_as_pdf(self):
if not self.background_pixmap:
return
options = QFileDialog.Options()
file_path, _ = QFileDialog.getSaveFileName(
self, "保存为PDF", "",
"PDF Files (*.pdf);;All Files (*)",
options=options
)
if file_path:
try:
if not file_path.lower().endswith('.pdf'):
file_path += '.pdf'
printer = QPrinter(QPrinter.HighResolution)
printer.setOutputFormat(QPrinter.PdfFormat)
printer.setOutputFileName(file_path)
printer.setResolution(self.high_res_dpi)
printer.setDocName("图像标注")
printer.setCreator("PDFAnnotationTool")
if self.background_pixmap.width() > self.background_pixmap.height():
printer.setOrientation(QPrinter.Landscape)
else:
printer.setOrientation(QPrinter.Portrait)
self.render_for_pdf(printer)
self.status_label.setText(f"已保存为: {os.path.basename(file_path)}")
QMessageBox.information(self, "保存成功", f"PDF文件已保存到:\n{file_path}")
except Exception as e:
self.show_error(f"保存PDF时出错: {str(e)}")
def render_for_pdf(self, printer):
# 获取打印机的DPI
printer_dpi = printer.resolution()
# 计算毫米到像素的转换因子
mm_to_pixel = printer_dpi / 25.4
# 计算逻辑线条宽度在打印机上的像素值
line_width_pixels = self.logical_line_width * mm_to_pixel
# 计算字体大小
font_size_pixels = self.logical_font_size * mm_to_pixel
painter = QPainter(printer)
painter.setRenderHint(QPainter.Antialiasing)
painter.setRenderHint(QPainter.TextAntialiasing)
painter.setRenderHint(QPainter.SmoothPixmapTransform)
# 计算页面可用区域
page_rect = printer.pageRect(QPrinter.DevicePixel)
page_width = page_rect.width()
page_height = page_rect.height()
# 保持图像原始分辨率
img_width = self.background_pixmap.width()
img_height = self.background_pixmap.height()
# 计算保持宽高比的最大尺寸
scale = min(page_width/img_width, page_height/img_height)
target_width = int(img_width * scale)
target_height = int(img_height * scale)
# 计算居中位置
x_offset = int((page_width - target_width) / 2)
y_offset = int((page_height - target_height) / 2)
# 创建高质量缩放图像
scaled_pixmap = self.background_pixmap.scaled(
target_width, target_height,
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
painter.drawPixmap(QPoint(x_offset, y_offset), scaled_pixmap)
# 计算缩放因子
scale_x = target_width / self.background_pixmap.width()
scale_y = target_height / self.background_pixmap.height()
# 创建统一的画笔
line_pen = QPen(Qt.red, line_width_pixels)
line_pen.setJoinStyle(Qt.RoundJoin)
line_pen.setCapStyle(Qt.RoundCap)
# 创建字体
font = QFont("Arial")
font.setPixelSize(int(round(font_size_pixels)))
# 创建变换矩阵
transform = QTransform()
transform.scale(scale_x, scale_y)
transform.translate(x_offset / scale_x, y_offset / scale_y)
# 渲染所有标注项
for item in self.scene.items():
if item == self.background_item:
continue
painter.save()
painter.setTransform(transform, True)
if isinstance(item, QGraphicsItemGroup):
for child in item.childItems():
if isinstance(child, QGraphicsPathItem) and not hasattr(child, 'is_text'):
path = child.path()
pos = child.pos()
painter.save()
painter.translate(pos.x(), pos.y())
painter.setPen(line_pen)
painter.setBrush(QBrush(QColor(255, 255, 255, 200)))
painter.drawPath(path)
painter.restore()
elif isinstance(child, QGraphicsPathItem) and hasattr(child, 'is_text'):
path = child.path()
pos = child.pos()
painter.save()
painter.translate(pos.x(), pos.y())
painter.setPen(Qt.NoPen)
painter.setBrush(QBrush(Qt.black))
painter.setFont(font)
painter.drawPath(path)
painter.restore()
elif isinstance(item, QGraphicsLineItem):
line = item.line()
painter.setPen(line_pen)
painter.drawLine(line)
painter.restore()
painter.end()
def show_error(self, message):
QMessageBox.critical(self, "错误", message)
def clear_all_annotations(self):
# 创建项目副本避免修改迭代
items = list(self.scene.items())
for item in items:
if item != self.background_item:
self.scene.removeItem(item)
self.annotation_groups = []
self.last_rect = None
self.circle_count = 1
self.sub_count = 1
def start_single_deletion(self):
self.is_deleting_single = True
self.is_annotating = False
self.status_label.setText("删除单个标注模式: 点击要删除的标注")
def start_annotation(self, mode):
self.is_deleting_single = False
self.annotation_mode = mode
if mode == 'continuous':
main_num, ok = QInputDialog.getInt(self, "设置初始序号", "请输入主序号:",
self.circle_count, 1, 100, 1)
if ok:
sub_num, ok = QInputDialog.getInt(self, "设置初始序号", "请输入子序号:",
self.sub_count, 1, 999, 1)
if ok:
self.circle_count = main_num
self.sub_count = sub_num
self.is_annotating = True
self.status_label.setText(f"连续标注模式: 当前序号 {self.circle_count}-{self.sub_count} (点击图像进行标注)")
elif mode == 'range':
range_text, ok = QInputDialog.getText(self, "设置范围标注", "请输入范围标注序号 (如: 2-17~2-27):")
if ok and range_text:
if '~' in range_text:
self.is_annotating = True
self.range_text = range_text
self.status_label.setText(f"范围标注模式: 当前序号 {range_text} (点击图像进行标注)")
else:
QMessageBox.warning(self, "输入错误", "请输入有效的范围标注格式 (如: 2-17~2-27)")
def exit_annotation_mode(self):
self.is_annotating = False
self.is_deleting_single = False
self.annotation_mode = None
self.last_rect = None
self.status_label.setText("标注模式已退出,可以平移和缩放视图")
def on_mouse_press(self, event):
if event.button() == Qt.RightButton:
self.exit_annotation_mode()
return
if event.button() == Qt.LeftButton and event.modifiers() & Qt.ControlModifier:
self.is_panning = True
self.last_mouse_pos = event.pos()
self.view.setCursor(Qt.ClosedHandCursor)
return
scene_pos = self.view.mapToScene(event.pos())
if event.button() == Qt.LeftButton and not self.is_panning:
if self.is_deleting_single:
items = self.scene.items(scene_pos, Qt.IntersectsItemShape, Qt.DescendingOrder, self.view.transform())
for item in items:
if isinstance(item, QGraphicsItemGroup):
self.delete_connected_lines(item)
for child in item.childItems():
self.scene.removeItem(child)
self.scene.removeItem(item)
self.annotation_groups = [g for g in self.annotation_groups if g['group'] != item]
break
elif isinstance(item, QGraphicsLineItem):
for group_info in self.annotation_groups:
if item in group_info['lines']:
self.scene.removeItem(item)
group_info['lines'].remove(item)
break
break
return
if not self.is_annotating:
self.view.setDragMode(QGraphicsView.ScrollHandDrag)
super(QGraphicsView, self.view).mousePressEvent(event)
return
self.view.setDragMode(QGraphicsView.NoDrag)
x, y = scene_pos.x(), scene_pos.y()
if self.last_rect is None:
self.create_pill_shape(x, y)
else:
self.create_line(x, y)
def on_mouse_move(self, event):
if self.is_panning and event.buttons() & Qt.LeftButton and self.last_mouse_pos is not None:
delta = event.pos() - self.last_mouse_pos
self.last_mouse_pos = event.pos()
h_bar = self.view.horizontalScrollBar()
v_bar = self.view.verticalScrollBar()
h_bar.setValue(h_bar.value() - delta.x())
v_bar.setValue(v_bar.value() - delta.y())
else:
super(QGraphicsView, self.view).mouseMoveEvent(event)
def on_mouse_release(self, event):
if event.button() == Qt.LeftButton and self.is_panning:
self.is_panning = False
self.last_mouse_pos = None
self.view.setCursor(Qt.ArrowCursor)
else:
super(QGraphicsView, self.view).mouseReleaseEvent(event)
def on_wheel_event(self, event):
if event.modifiers() & Qt.ControlModifier:
zoom_factor = 1.15
if event.angleDelta().y() > 0:
self.view.scale(zoom_factor, zoom_factor)
else:
self.view.scale(1.0 / zoom_factor, 1.0 / zoom_factor)
else:
super(QGraphicsView, self.view).wheelEvent(event)
def delete_connected_lines(self, group_item):
for group_info in self.annotation_groups:
if group_info['group'] == group_item:
for line in group_info['lines']:
self.scene.removeItem(line)
self.annotation_groups.remove(group_info)
break
def create_pill_shape(self, x, y):
if self.annotation_mode == 'continuous':
label = f"{self.circle_count}-{self.sub_count}"
self.sub_count += 1
self.status_label.setText(f"连续标注模式: 当前序号 {self.circle_count}-{self.sub_count} (点击图像进行标注)")
else:
label = self.range_text
# 计算屏幕上的字体大小(逻辑单位转换为像素)
font_size_pixels = self.logical_font_size * self.screen_dpi / 25.4
# 创建字体对象
font = QFont("Arial")
font.setPixelSize(int(round(font_size_pixels)))
# 使用QFontMetrics计算文本尺寸
fm = QFontMetrics(font)
text_width = fm.width(label)
text_height = fm.height()
# 计算胶囊尺寸
margin = 6
height = text_height + margin * 2
radius = height / 2
width = max(text_width + margin * 2, height)
# 创建胶囊路径
path = QPainterPath()
path.moveTo(x - width/2 + radius, y - height/2)
path.arcTo(x - width/2, y - height/2, height, height, 90, 180)
path.lineTo(x + width/2 - radius, y + height/2)
path.arcTo(x + width/2 - height, y - height/2, height, height, 270, 180)
path.lineTo(x - width/2 + radius, y - height/2)
# 计算屏幕上的线条宽度(逻辑单位转换为像素)
line_width_pixels = self.logical_line_width * self.screen_dpi / 25.4
# 创建胶囊图形项
shape = QGraphicsPathItem(path)
shape.setPen(QPen(Qt.red, line_width_pixels))
shape.setBrush(QBrush(QColor(255, 255, 255, 200)))
# 创建文本路径项
text_path = QPainterPath()
# 计算文本位置(居中)
text_x = x - text_width / 2
text_y = y + text_height / 4 # 微调垂直位置
text_path.addText(text_x, text_y, font, label)
# 创建文本路径图形项 - 修复画笔设置
text_item = QGraphicsPathItem(text_path)
text_item.setPen(QPen(Qt.NoPen)) # 正确设置无画笔
text_item.setBrush(QBrush(Qt.black)) # 设置黑色填充
text_item.is_text = True # 标记为文本路径
# 创建组并添加到场景
group = self.scene.createItemGroup([shape, text_item])
self.last_rect = group
self.current_rect = group
# 保存标注信息
self.annotation_groups.append({
'group': group,
'lines': []
})
def create_line(self, x, y):
if self.last_rect is None:
return
shape_rect = self.last_rect.boundingRect()
shape_center = self.last_rect.mapToScene(shape_rect.center())
path_item = None
for item in self.last_rect.childItems():
if isinstance(item, QGraphicsPathItem) and not hasattr(item, 'is_text'):
path_item = item
break
if path_item is None:
return
original_path = path_item.path()
calc_path = QPainterPath()
calc_path.addPath(original_path)
calc_path.translate(path_item.pos())
dx = x - shape_center.x()
dy = y - shape_center.y()
distance = math.sqrt(dx*dx + dy*dy)
if distance == 0:
return
direction_x = dx / distance
direction_y = dy / distance
low = 0.0
high = distance
epsilon = 0.1
intersect_point = None
# 二分法计算交点
for _ in range(20):
mid = (low + high) / 2
test_point = QPointF(
shape_center.x() + direction_x * mid,
shape_center.y() + direction_y * mid
)
if calc_path.contains(test_point):
low = mid
else:
high = mid
if high - low < epsilon:
intersect_point = QPointF(
shape_center.x() + direction_x * low,
shape_center.y() + direction_y * low
)
break
if intersect_point is None:
intersect_point = QPointF(
shape_center.x() + direction_x * low,
shape_center.y() + direction_y * low
)
# 计算屏幕上的线条宽度(逻辑单位转换为像素)
line_width_pixels = self.logical_line_width * self.screen_dpi / 25.4
# 创建直线
line = QGraphicsLineItem(
intersect_point.x(), intersect_point.y(),
x, y
)
line.setPen(QPen(Qt.red, line_width_pixels))
self.scene.addItem(line)
# 保存直线到组
for group_info in self.annotation_groups:
if group_info['group'] == self.last_rect:
group_info['lines'].append(line)
break
self.last_rect = None
# 范围标注模式完成一次标注后退出
if self.annotation_mode == 'range':
self.is_annotating = False
self.status_label.setText("标注完成")
if __name__ == "__main__":
app = QApplication(sys.argv)
# 设置应用程序的视觉样式
app.setStyle("Fusion")
# 创建调色板
palette = QPalette()
palette.setColor(QPalette.Window, QColor(53, 53, 53))
palette.setColor(QPalette.WindowText, Qt.white)
palette.setColor(QPalette.Base, QColor(25, 25, 25))
palette.setColor(QPalette.AlternateBase, QColor(53, 53, 53))
palette.setColor(QPalette.ToolTipBase, Qt.white)
palette.setColor(QPalette.ToolTipText, Qt.white)
palette.setColor(QPalette.Text, Qt.white)
palette.setColor(QPalette.Button, QColor(53, 53, 53))
palette.setColor(QPalette.ButtonText, Qt.white)
palette.setColor(QPalette.BrightText, Qt.red)
palette.setColor(QPalette.Link, QColor(42, 130, 218))
palette.setColor(QPalette.Highlight, QColor(42, 130, 218))
palette.setColor(QPalette.HighlightedText, Qt.black)
app.setPalette(palette)
window = PDFAnnotationTool()
window.show()
sys.exit(app.exec_())