完成图片框选功能

This commit is contained in:
Voge1imkafig 2025-08-12 18:03:00 +08:00
parent cc460cfbcd
commit f2c782a542
2 changed files with 396 additions and 265 deletions

View File

@ -1,196 +1,308 @@
from PySide6.QtCore import Qt, QPointF, QRectF, Signal from PySide6.QtCore import Qt, QPointF, QRectF, Signal, QSize, QLineF
from PySide6.QtGui import QPixmap, QPainter, QPen, QBrush, QCursor, QColor, QPainterPath from PySide6.QtGui import (QPixmap, QPainter, QPen, QBrush, QCursor, QColor, QImage,
QPainterPath, QTransform)
from PySide6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, from PySide6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton,
QGraphicsView, QGraphicsScene, QGraphicsItem, QGraphicsView, QGraphicsScene, QGraphicsItem,
QGraphicsRectItem, QGraphicsEllipseItem, QGraphicsRectItem, QGraphicsEllipseItem,
QGraphicsPixmapItem, QSizePolicy) QGraphicsPixmapItem, QSizePolicy)
import math
class ResizableGraphicsItem(QGraphicsRectItem): class ResizableGraphicsItem(QGraphicsRectItem):
def __init__(self, x, y, width, height, parent=None): handle_radius = 12 # 手柄半径(视觉+点击区域)
super().__init__(x, y, width, height, parent) rotate_offset = 0 # 旋转手柄离矩形顶边的距离
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.ItemIsMovable, True) def __init__(self, x, y, w, h, parent=None):
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True) super().__init__(x, y, w, h, parent)
self.setFlags(QGraphicsItem.ItemIsSelectable |
QGraphicsItem.ItemIsMovable |
QGraphicsItem.ItemSendsGeometryChanges)
self.setAcceptHoverEvents(True) self.setAcceptHoverEvents(True)
self.setPen(QPen(Qt.blue, 2))
self.handle_size = 8 self.setBrush(QBrush(Qt.transparent))
self.handles = {}
self.selected_handle = None self.setTransformOriginPoint(self.rect().center())
self.mouse_press_pos = None
self.mouse_press_rect = None self._rotating = False
self._resize_corner = None
# 创建调整大小的手柄 self._orig_rect = QRectF()
self.create_handles() self._press_scene = QPointF()
self._orig_rotation = 0.0
def create_handles(self):
"""创建调整大小的手柄""" self._rotate_anchor = QPointF() # 旋转中心item 坐标)
rect = self.rect() self._last_vec = QPointF() # 上一帧鼠标相对向量
self.handles = {
"top_left": QRectF(rect.left(), rect.top(), self.handle_size, self.handle_size), def setRect(self, rect):
"top_right": QRectF(rect.right() - self.handle_size, rect.top(), self.handle_size, self.handle_size), super().setRect(rect)
"bottom_left": QRectF(rect.left(), rect.bottom() - self.handle_size, self.handle_size, self.handle_size), self.setTransformOriginPoint(rect.center())
"bottom_right": QRectF(rect.right() - self.handle_size, rect.bottom() - self.handle_size, self.handle_size, self.handle_size),
"top": QRectF(rect.center().x() - self.handle_size/2, rect.top(), self.handle_size, self.handle_size),
"bottom": QRectF(rect.center().x() - self.handle_size/2, rect.bottom() - self.handle_size, self.handle_size, self.handle_size),
"left": QRectF(rect.left(), rect.center().y() - self.handle_size/2, self.handle_size, self.handle_size),
"right": QRectF(rect.right() - self.handle_size, rect.center().y() - self.handle_size/2, self.handle_size, self.handle_size),
}
def update_handles(self):
"""更新手柄位置"""
rect = self.rect()
self.handles["top_left"].moveTopLeft(rect.topLeft())
self.handles["top_right"].moveTopRight(rect.topRight())
self.handles["bottom_left"].moveBottomLeft(rect.bottomLeft())
self.handles["bottom_right"].moveBottomRight(rect.bottomRight())
self.handles["top"].moveCenter(QPointF(rect.center().x(), rect.top() + self.handle_size/2))
self.handles["bottom"].moveCenter(QPointF(rect.center().x(), rect.bottom() - self.handle_size/2))
self.handles["left"].moveCenter(QPointF(rect.left() + self.handle_size/2, rect.center().y()))
self.handles["right"].moveCenter(QPointF(rect.right() - self.handle_size/2, rect.center().y()))
def paint(self, painter, option, widget=None): def paint(self, painter, option, widget=None):
"""绘制项和手柄""" super().paint(painter, option, widget)
# 绘制主矩形 if not self.isSelected():
pen = QPen(Qt.blue, 2, Qt.SolidLine) return
if self.isSelected():
pen.setStyle(Qt.DashLine) painter.save()
painter.setPen(pen) painter.setRenderHint(QPainter.Antialiasing) # 消除残影
painter.setBrush(QBrush(Qt.transparent)) painter.setPen(QPen(Qt.black, 1))
painter.drawRect(self.rect())
for name, pt in self._handles_local().items():
color = Qt.red if name == 'rotate' else Qt.green
painter.setBrush(QBrush(color))
painter.drawEllipse(pt, self.handle_radius, self.handle_radius)
painter.restore()
def drawTransformHandles(self, painter):
"""绘制变换控制点"""
rect = self.rect()
handle_size = self.handle_radius * 2
# 如果选中,绘制手柄 # 四个角的控制点
if self.isSelected(): corners = [
painter.setBrush(QBrush(Qt.white)) rect.topLeft(),
painter.setPen(QPen(Qt.black, 1)) rect.topRight(),
for handle in self.handles.values(): rect.bottomLeft(),
painter.drawRect(handle) rect.bottomRight()
]
def hoverMoveEvent(self, event):
"""鼠标悬停时检查是否在手柄上"""
for handle_name, handle_rect in self.handles.items():
if handle_rect.contains(event.pos()):
# 根据手柄位置设置不同的光标
if handle_name in ["top_left", "bottom_right"]:
self.setCursor(Qt.SizeFDiagCursor)
elif handle_name in ["top_right", "bottom_left"]:
self.setCursor(Qt.SizeBDiagCursor)
elif handle_name in ["top", "bottom"]:
self.setCursor(Qt.SizeVerCursor)
elif handle_name in ["left", "right"]:
self.setCursor(Qt.SizeHorCursor)
self.selected_handle = handle_name
return
self.setCursor(Qt.SizeAllCursor) # 旋转控制点(顶部中间)
self.selected_handle = None rotate_pos = QPointF(rect.center().x(), rect.top() - 20)
super().hoverMoveEvent(event)
def hoverLeaveEvent(self, event):
"""鼠标离开时恢复默认光标"""
self.setCursor(Qt.ArrowCursor)
self.selected_handle = None
super().hoverLeaveEvent(event)
def mousePressEvent(self, event):
"""鼠标按下事件"""
if event.button() == Qt.LeftButton:
self.mouse_press_pos = event.pos()
self.mouse_press_rect = self.rect()
# 如果点击的是手柄,则开始调整大小
if self.selected_handle:
event.accept()
return
super().mousePressEvent(event) # 绘制控制点
painter.setBrush(QBrush(Qt.green))
painter.setPen(QPen(Qt.black, 1))
for corner in corners:
painter.drawEllipse(corner, handle_size, handle_size)
# 绘制旋转控制点
painter.setBrush(QBrush(Qt.red))
painter.drawEllipse(rotate_pos, handle_size, handle_size)
def mouseMoveEvent(self, event): def mouseMoveEvent(self, event):
"""鼠标移动事件""" if self._rotating:
if event.buttons() & Qt.LeftButton and self.mouse_press_pos: # 当前鼠标向量
if self.selected_handle: cur_vec = event.scenePos() - self._rotate_center
# 调整大小 # 计算角度增量
self.resize_item(event.pos()) old_angle = math.atan2(self._press_vec.y(), self._press_vec.x())
event.accept() new_angle = math.atan2(cur_vec.y(), cur_vec.x())
return delta = math.degrees(new_angle - old_angle)
else:
# 移动项 # 直接累加旋转角Qt 会按 TransformOriginPoint 旋转
super().mouseMoveEvent(event) self.setRotation(self.rotation() + delta)
else:
super().mouseMoveEvent(event) self._press_vec = cur_vec
event.accept()
def resize_item(self, mouse_pos): return
"""根据鼠标位置调整项大小"""
rect = self.mouse_press_rect if self._resize_corner:
pos = mouse_pos delta = event.pos() - self._press_local
new_rect = QRectF(rect) r = self._orig_rect
new_r = QRectF(r)
if self._resize_corner == 'topLeft':
new_r.setTopLeft(r.topLeft() + delta)
elif self._resize_corner == 'topRight':
new_r.setTopRight(r.topRight() + delta)
elif self._resize_corner == 'bottomLeft':
new_r.setBottomLeft(r.bottomLeft() + delta)
elif self._resize_corner == 'bottomRight':
new_r.setBottomRight(r.bottomRight() + delta)
if new_r.width() >= 10 and new_r.height() >= 10:
self.setRect(new_r)
event.accept()
return
super().mouseMoveEvent(event)
def mousePressEvent(self, event):
if event.button() != Qt.LeftButton:
super().mousePressEvent(event)
return
hit = self._hit_handle(event.pos())
if hit == 'rotate':
self._rotating = True
# 记录旋转中心(场景坐标)
self._rotate_center = self.mapToScene(self.rect().center())
# 记录鼠标第一帧向量(场景坐标)
self._press_vec = event.scenePos() - self._rotate_center
event.accept()
return
if hit in ('topLeft', 'topRight', 'bottomLeft', 'bottomRight'):
self._resize_corner = hit
self._orig_rect = self.rect()
self._press_local = event.pos()
event.accept()
return
# 根据选中的手柄调整矩形 if hit == 'rotate':
if self.selected_handle == "top_left": self._rotating = True
new_rect.setTopLeft(pos) self._rotate_anchor = self.rect().center() # 旋转中心
elif self.selected_handle == "top_right": self._last_vec = event.pos() - self._rotate_anchor # 第一帧向量
new_rect.setTopRight(pos) event.accept()
elif self.selected_handle == "bottom_left": return
new_rect.setBottomLeft(pos)
elif self.selected_handle == "bottom_right": super().mousePressEvent(event)
new_rect.setBottomRight(pos)
elif self.selected_handle == "top":
new_rect.setTop(pos.y())
elif self.selected_handle == "bottom":
new_rect.setBottom(pos.y())
elif self.selected_handle == "left":
new_rect.setLeft(pos.x())
elif self.selected_handle == "right":
new_rect.setRight(pos.x())
# 确保矩形不会太小
if new_rect.width() < 10:
if self.selected_handle in ["left", "top_left", "bottom_left"]:
new_rect.setLeft(new_rect.right() - 10)
else:
new_rect.setRight(new_rect.left() + 10)
if new_rect.height() < 10:
if self.selected_handle in ["top", "top_left", "top_right"]:
new_rect.setTop(new_rect.bottom() - 10)
else:
new_rect.setBottom(new_rect.top() + 10)
self.setRect(new_rect)
self.update_handles()
def mouseReleaseEvent(self, event): def mouseReleaseEvent(self, event):
"""鼠标释放事件""" self._rotating = False
self.mouse_press_pos = None self._resize_corner = None
self.mouse_press_rect = None self.setCursor(Qt.ArrowCursor)
self.selected_handle = None
super().mouseReleaseEvent(event) super().mouseReleaseEvent(event)
def itemChange(self, change, value): def hoverMoveEvent(self, event):
"""项变化时更新手柄位置""" if not self.isSelected():
if change == QGraphicsItem.ItemSelectedChange: super().hoverMoveEvent(event)
self.update_handles() return
elif change == QGraphicsItem.ItemPositionHasChanged or change == QGraphicsItem.ItemTransformHasChanged: hit = self._hit_handle(event.pos())
self.update_handles() self.setCursor(Qt.PointingHandCursor if hit == 'rotate' else
Qt.SizeFDiagCursor if hit in ('topLeft', 'bottomRight') else
return super().itemChange(change, value) Qt.SizeBDiagCursor if hit in ('topRight', 'bottomLeft') else
Qt.ArrowCursor)
super().hoverMoveEvent(event)
def _handles_local(self):
r = self.rect()
return {
'topLeft' : r.topLeft(),
'topRight' : r.topRight(),
'bottomLeft' : r.bottomLeft(),
'bottomRight': r.bottomRight(),
'rotate' : QPointF(r.center().x(), r.top() - self.rotate_offset)
}
def boundingRect(self):
m = self.handle_radius + 5
return self.rect().adjusted(-m, -m - self.rotate_offset, m, m)
def _handles(self):
"""返回所有手柄的 scene 坐标"""
rect = self.rect()
center = self.mapToScene(rect.center())
top_mid = self.mapToScene(QPointF(rect.center().x(), rect.top()))
rotate = top_mid + QPointF(0, -20) # 旋转手柄
return {
'topLeft' : self.mapToScene(rect.topLeft()),
'topRight' : self.mapToScene(rect.topRight()),
'bottomLeft' : self.mapToScene(rect.bottomLeft()),
'bottomRight': self.mapToScene(rect.bottomRight()),
'rotate' : rotate
}
def _hit_handle(self, pos_local):
for name, pt in self._handles_local().items():
if (pos_local - pt).manhattanLength() <= self.handle_radius:
return name
return None
class ResizableEllipseItem(ResizableGraphicsItem): class ResizableEllipseItem(ResizableGraphicsItem):
def paint(self, painter, option, widget=None): def paint(self, painter, option, widget=None):
"""绘制椭圆和手柄""" # 先画椭圆
# 绘制主椭圆 painter.setPen(QPen(Qt.red, 2))
pen = QPen(Qt.red, 2, Qt.SolidLine) painter.setBrush(Qt.NoBrush)
if self.isSelected():
pen.setStyle(Qt.DashLine)
painter.setPen(pen)
painter.setBrush(QBrush(Qt.transparent))
painter.drawEllipse(self.rect()) painter.drawEllipse(self.rect())
# 如果选中,绘制手柄 # 选中时画控制手柄
if self.isSelected(): if self.isSelected():
painter.setBrush(QBrush(Qt.white)) self.drawTransformHandles(painter)
painter.setPen(QPen(Qt.black, 1))
for handle in self.handles.values(): class CustomGraphicsView(QGraphicsView):
painter.drawRect(handle) def __init__(self, scene, editor, parent=None):
super().__init__(scene, parent)
self.editor = editor
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self._pan = False
self._pan_start = QPointF()
self._last_pan_value = QPointF()
def wheelEvent(self, event):
"""鼠标滚轮缩放"""
factor = 1.2
if event.angleDelta().y() < 0:
factor = 1.0 / factor
self.scale(factor, factor)
self.editor.scale_factor *= factor
self.limit_view_to_image()
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
scene_pos = self.mapToScene(event.position().toPoint())
items = self.scene().items(scene_pos)
# 如果点到了框线,让框线处理,且禁止 pan
if any(isinstance(it, (ResizableGraphicsItem, ResizableEllipseItem))
for it in items):
super().mousePressEvent(event)
return
# 选择模式下点空白,启动 pan
if self.editor.mouse_state == "select":
self.scene().clearSelection() # 取消所有选中
self._pan = True
self._pan_start = event.position()
self.setCursor(Qt.ClosedHandCursor)
return
if self.editor.mouse_state in ["create_rect", "create_ellipse"]:
self.editor.start_pos = scene_pos
if self.editor.mouse_state == "create_rect":
self.editor.current_item = ResizableGraphicsItem(0, 0, 0, 0)
else:
self.editor.current_item = ResizableEllipseItem(0, 0, 0, 0)
self.editor.current_item.setRect(QRectF(scene_pos, scene_pos))
self.scene().addItem(self.editor.current_item)
self.editor.current_item.setSelected(True)
return
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self._pan:
delta = event.position() - self._pan_start
self._pan_start = event.position()
hs = self.horizontalScrollBar()
vs = self.verticalScrollBar()
hs.setValue(hs.value() - int(delta.x()))
vs.setValue(vs.value() - int(delta.y()))
return
if self.editor.current_item and self.editor.start_pos:
scene_pos = self.mapToScene(event.position().toPoint())
rect = QRectF(self.editor.start_pos, scene_pos).normalized()
self.editor.current_item.setRect(rect)
return
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if event.button() == Qt.MiddleButton or self._pan:
self._pan = False
self.setCursor(Qt.ArrowCursor)
return
if self.editor.current_item:
if self.editor.current_item.rect().width() < 10 or self.editor.current_item.rect().height() < 10:
self.scene().removeItem(self.editor.current_item)
else:
self.editor.set_select_mode()
self.editor.current_item = None
self.editor.start_pos = None
return
super().mouseReleaseEvent(event)
def limit_view_to_image(self):
"""限制视图范围不超过图片边界"""
if not self.editor.pixmap_item:
return
# 确保滚动条不会超出范围
h_scroll = self.horizontalScrollBar()
v_scroll = self.verticalScrollBar()
h_scroll.setValue(max(0, min(h_scroll.value(), h_scroll.maximum())))
v_scroll.setValue(max(0, min(v_scroll.value(), v_scroll.maximum())))
class DefectMarkEditor(QDialog): class DefectMarkEditor(QDialog):
def __init__(self, parent=None, image_path="", current_description=""): def __init__(self, parent=None, image_path="", current_description=""):
@ -204,7 +316,11 @@ class DefectMarkEditor(QDialog):
self.start_pos = None self.start_pos = None
self.current_item = None self.current_item = None
self.scale_factor = 1.0 self.scale_factor = 1.0
self.is_panning = False
self.pan_start_pos = QPointF()
self.max_view_size = QSize(1920, 1080) # 最大视图尺寸
self.pixmap_item = None
self.init_ui() self.init_ui()
self.load_image() self.load_image()
@ -239,8 +355,8 @@ class DefectMarkEditor(QDialog):
self.scene = QGraphicsScene(self) self.scene = QGraphicsScene(self)
self.view = CustomGraphicsView(self.scene, self) self.view = CustomGraphicsView(self.scene, self)
self.view.setRenderHint(QPainter.Antialiasing) self.view.setRenderHint(QPainter.Antialiasing)
self.view.setDragMode(QGraphicsView.RubberBandDrag)
self.view.setMouseTracking(True) self.view.setMouseTracking(True)
self.view.setDragMode(QGraphicsView.NoDrag)
main_layout.addLayout(toolbar) main_layout.addLayout(toolbar)
main_layout.addWidget(self.view) main_layout.addWidget(self.view)
@ -251,13 +367,32 @@ class DefectMarkEditor(QDialog):
return return
self.scene.clear() self.scene.clear()
self.pixmap_item = QGraphicsPixmapItem(QPixmap(self.image_path))
# 加载图片并计算合适的缩放比例
image = QImage(self.image_path)
if image.isNull():
return
# 计算缩放比例以适应视图
img_size = image.size()
# 如果图片大于最大视图尺寸,则缩放
if img_size.width() > self.max_view_size.width() or img_size.height() > self.max_view_size.height():
img_size.scale(self.max_view_size, Qt.KeepAspectRatio)
scaled_pixmap = QPixmap.fromImage(image.scaled(img_size, Qt.KeepAspectRatio, Qt.SmoothTransformation))
else:
scaled_pixmap = QPixmap.fromImage(image)
self.pixmap_item = QGraphicsPixmapItem(scaled_pixmap)
self.pixmap_item.setFlag(QGraphicsItem.ItemIsMovable, False) self.pixmap_item.setFlag(QGraphicsItem.ItemIsMovable, False)
self.pixmap_item.setFlag(QGraphicsItem.ItemIsSelectable, False) self.pixmap_item.setFlag(QGraphicsItem.ItemIsSelectable, False)
self.scene.addItem(self.pixmap_item) self.scene.addItem(self.pixmap_item)
# 设置场景大小为图片大小 # 设置场景大小为图片大小
self.scene.setSceneRect(self.pixmap_item.boundingRect()) self.scene.setSceneRect(self.pixmap_item.boundingRect())
# 自动调整视图以显示整个图片
self.view.fitInView(self.pixmap_item, Qt.KeepAspectRatio)
def set_select_mode(self): def set_select_mode(self):
"""设置为选择模式""" """设置为选择模式"""
@ -265,7 +400,7 @@ class DefectMarkEditor(QDialog):
self.select_btn.setChecked(True) self.select_btn.setChecked(True)
self.rect_btn.setChecked(False) self.rect_btn.setChecked(False)
self.ellipse_btn.setChecked(False) self.ellipse_btn.setChecked(False)
self.view.setDragMode(QGraphicsView.RubberBandDrag) self.view.setCursor(Qt.ArrowCursor)
def set_create_rect_mode(self): def set_create_rect_mode(self):
"""设置为创建矩形模式""" """设置为创建矩形模式"""
@ -273,7 +408,7 @@ class DefectMarkEditor(QDialog):
self.select_btn.setChecked(False) self.select_btn.setChecked(False)
self.rect_btn.setChecked(True) self.rect_btn.setChecked(True)
self.ellipse_btn.setChecked(False) self.ellipse_btn.setChecked(False)
self.view.setDragMode(QGraphicsView.NoDrag) self.view.setCursor(Qt.CrossCursor)
def set_create_ellipse_mode(self): def set_create_ellipse_mode(self):
"""设置为创建圆形模式""" """设置为创建圆形模式"""
@ -281,94 +416,90 @@ class DefectMarkEditor(QDialog):
self.select_btn.setChecked(False) self.select_btn.setChecked(False)
self.rect_btn.setChecked(False) self.rect_btn.setChecked(False)
self.ellipse_btn.setChecked(True) self.ellipse_btn.setChecked(True)
self.view.setDragMode(QGraphicsView.NoDrag) self.view.setCursor(Qt.CrossCursor)
def clear_all_items(self): def clear_all_items(self):
"""清除所有标记项""" """清除所有标记项"""
for item in self.scene.items(): for item in self.scene.items():
if isinstance(item, (ResizableGraphicsItem, ResizableEllipseItem)): if isinstance(item, (ResizableGraphicsItem, ResizableEllipseItem)):
self.scene.removeItem(item) self.scene.removeItem(item)
def mouse_press_on_scene(self, pos):
"""场景鼠标按下事件"""
if self.mouse_state == "select":
return
self.start_pos = pos
if self.mouse_state == "create_rect":
self.current_item = ResizableGraphicsItem(pos.x(), pos.y(), 1, 1)
elif self.mouse_state == "create_ellipse":
self.current_item = ResizableEllipseItem(pos.x(), pos.y(), 1, 1)
if self.current_item:
self.scene.addItem(self.current_item)
def mouse_move_on_scene(self, pos):
"""场景鼠标移动事件"""
if not self.start_pos or not self.current_item:
return
rect = QRectF(self.start_pos, pos).normalized()
self.current_item.setRect(rect)
def mouse_release_on_scene(self, pos):
"""场景鼠标释放事件"""
if self.current_item and self.mouse_state != "select":
# 如果创建的项太小,则删除
if self.current_item.rect().width() < 10 or self.current_item.rect().height() < 10:
self.scene.removeItem(self.current_item)
self.start_pos = None
self.current_item = None
# 创建完成后自动回到选择模式
self.set_select_mode()
class CustomGraphicsView(QGraphicsView): def export_annotated_image(self, output_dir):
def __init__(self, scene, editor, parent=None): """
super().__init__(scene, parent) 将当前场景原图 + 所有标记按原分辨率导出为 PNG/JPG
self.editor = editor 文件名与原图完全一致保存到 output_dir
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) """
self.setResizeAnchor(QGraphicsView.AnchorUnderMouse) import os
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) from PySide6.QtGui import QPainter, QImage
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) if not self.pixmap_item:
return False
# 1. 取原图 QImage保持原始分辨率
source_image = QImage(self.image_path)
if source_image.isNull():
return False
# 2. 创建与原图同尺寸的 QImage 作为画布
out_image = QImage(source_image.size(), QImage.Format_ARGB32)
out_image.fill(Qt.transparent)
painter = QPainter(out_image)
painter.setRenderHint(QPainter.Antialiasing)
# 3. 先把原图完整画上去
painter.drawImage(0, 0, source_image)
# 4. 计算场景 → 原图坐标 的缩放因子
scene_rect = self.pixmap_item.sceneBoundingRect()
sx = source_image.width() / scene_rect.width()
sy = source_image.height() / scene_rect.height()
# 5. 逐个绘制标记(仅最终形状,不绘制手柄)
for item in self.scene.items():
if isinstance(item, (ResizableGraphicsItem, ResizableEllipseItem)):
# 关闭选中状态,避免手柄被绘制
was_selected = item.isSelected()
item.setSelected(False)
# 计算 item 在场景中的实际几何
shape_path = item.mapToScene(item.shape()) # 考虑旋转
painter.setTransform(QTransform()) # 重置 painter
painter.scale(sx, sy) # 映射到原图坐标
painter.translate(-scene_rect.x(), -scene_rect.y()) # 对齐偏移
# 设置画笔
pen = item.pen()
pen.setCosmetic(False) # 让线宽随缩放
painter.setPen(pen)
painter.setBrush(Qt.NoBrush)
# 绘制形状
if isinstance(item, ResizableEllipseItem):
painter.drawPath(shape_path)
else:
painter.drawPath(shape_path)
# 恢复选中状态
item.setSelected(was_selected)
painter.end()
# 6. 保存文件
os.makedirs(output_dir, exist_ok=True)
base_name = os.path.basename(self.image_path)
save_path = os.path.join(output_dir, base_name)
return out_image.save(save_path)
def wheelEvent(self, event): def closeEvent(self, event):
"""鼠标滚轮缩放""" """关闭窗口时保存标记"""
factor = 1.2 self.export_annotated_image("/home/dtyx/桌面/yhh/ReportGenerator/output")
if event.angleDelta().y() < 0: super().closeEvent(event)
factor = 1.0 / factor
if __name__ == '__main__':
self.scale(factor, factor) from PySide6.QtWidgets import QApplication
self.editor.scale_factor *= factor import sys
app = QApplication(sys.argv)
def mousePressEvent(self, event): editor = DefectMarkEditor(image_path=r"/home/dtyx/桌面/yhh/ReportGenerator/测试数据/山东国华无棣风电场叶片外部数据/A131301/1#1-GW68.6C-I190809C/DJI_20250606162745_0005_Z.JPG")
"""鼠标按下事件""" editor.show()
if event.button() == Qt.LeftButton: sys.exit(app.exec())
scene_pos = self.mapToScene(event.pos())
items = self.scene().items(scene_pos)
# 如果点击的是背景或图片,则开始创建
if not items or items[-1] == self.editor.pixmap_item:
self.editor.mouse_press_on_scene(scene_pos)
else:
super().mousePressEvent(event)
else:
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
"""鼠标移动事件"""
if self.editor.start_pos and self.editor.current_item:
scene_pos = self.mapToScene(event.pos())
self.editor.mouse_move_on_scene(scene_pos)
else:
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
"""鼠标释放事件"""
if event.button() == Qt.LeftButton and self.editor.start_pos:
scene_pos = self.mapToScene(event.pos())
self.editor.mouse_release_on_scene(scene_pos)
else:
super().mouseReleaseEvent(event)

View File

@ -2586,7 +2586,7 @@ class DictionaryBrowser(QWidget):
current_desc = self.current_dict_data.get(key, "") if key in self.current_dict_data else "" current_desc = self.current_dict_data.get(key, "") if key in self.current_dict_data else ""
# 创建并显示编辑对话框 # 创建并显示编辑对话框
editor = DefectDescriptionEditor(self, current_description=current_desc) editor = DefectDescriptionEditor(self, current_description=current_desc, img_path=img_path)
if editor.exec_() == QDialog.Accepted: if editor.exec_() == QDialog.Accepted:
# 获取新描述 # 获取新描述
new_desc = editor.get_description() new_desc = editor.get_description()
@ -2648,7 +2648,7 @@ class DefectDescriptionEditor(QDialog):
main_layout.setContentsMargins(5, 5, 5, 5) main_layout.setContentsMargins(5, 5, 5, 5)
# 左侧 - 图片编辑器 (使用DefectMarkEditor) # 左侧 - 图片编辑器 (使用DefectMarkEditor)
self.image_editor = DefectMarkEditor(self, self.img_path) self.image_editor = DefectMarkEditor(image_path=self.img_path)
self.image_editor.setMinimumSize(600, 600) self.image_editor.setMinimumSize(600, 600)
main_layout.addWidget(self.image_editor, stretch=2) main_layout.addWidget(self.image_editor, stretch=2)