完成图片框选功能

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.setBrush(QBrush(Qt.transparent))
self.handle_size = 8 self.setTransformOriginPoint(self.rect().center())
self.handles = {}
self.selected_handle = None
self.mouse_press_pos = None
self.mouse_press_rect = None
# 创建调整大小的手柄 self._rotating = False
self.create_handles() self._resize_corner = None
self._orig_rect = QRectF()
self._press_scene = QPointF()
self._orig_rotation = 0.0
def create_handles(self): self._rotate_anchor = QPointF() # 旋转中心item 坐标)
"""创建调整大小的手柄""" self._last_vec = QPointF() # 上一帧鼠标相对向量
rect = self.rect()
self.handles = {
"top_left": QRectF(rect.left(), rect.top(), self.handle_size, self.handle_size),
"top_right": QRectF(rect.right() - self.handle_size, rect.top(), self.handle_size, self.handle_size),
"bottom_left": QRectF(rect.left(), rect.bottom() - self.handle_size, self.handle_size, self.handle_size),
"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): def setRect(self, rect):
"""更新手柄位置""" super().setRect(rect)
rect = self.rect() self.setTransformOriginPoint(rect.center())
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.setPen(pen)
painter.setBrush(QBrush(Qt.transparent))
painter.drawRect(self.rect())
# 如果选中,绘制手柄 painter.save()
if self.isSelected(): painter.setRenderHint(QPainter.Antialiasing) # 消除残影
painter.setBrush(QBrush(Qt.white)) painter.setPen(QPen(Qt.black, 1))
painter.setPen(QPen(Qt.black, 1))
for handle in self.handles.values():
painter.drawRect(handle)
def hoverMoveEvent(self, event): for name, pt in self._handles_local().items():
"""鼠标悬停时检查是否在手柄上""" color = Qt.red if name == 'rotate' else Qt.green
for handle_name, handle_rect in self.handles.items(): painter.setBrush(QBrush(color))
if handle_rect.contains(event.pos()): painter.drawEllipse(pt, self.handle_radius, self.handle_radius)
# 根据手柄位置设置不同的光标 painter.restore()
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) def drawTransformHandles(self, painter):
self.selected_handle = None """绘制变换控制点"""
super().hoverMoveEvent(event) rect = self.rect()
handle_size = self.handle_radius * 2
def hoverLeaveEvent(self, event): # 四个角的控制点
"""鼠标离开时恢复默认光标""" corners = [
self.setCursor(Qt.ArrowCursor) rect.topLeft(),
self.selected_handle = None rect.topRight(),
super().hoverLeaveEvent(event) rect.bottomLeft(),
rect.bottomRight()
]
# 旋转控制点(顶部中间)
rotate_pos = QPointF(rect.center().x(), rect.top() - 20)
# 绘制控制点
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):
if self._rotating:
# 当前鼠标向量
cur_vec = event.scenePos() - self._rotate_center
# 计算角度增量
old_angle = math.atan2(self._press_vec.y(), self._press_vec.x())
new_angle = math.atan2(cur_vec.y(), cur_vec.x())
delta = math.degrees(new_angle - old_angle)
# 直接累加旋转角Qt 会按 TransformOriginPoint 旋转
self.setRotation(self.rotation() + delta)
self._press_vec = cur_vec
event.accept()
return
if self._resize_corner:
delta = event.pos() - self._press_local
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): def mousePressEvent(self, event):
"""鼠标按下事件""" if event.button() != Qt.LeftButton:
if event.button() == Qt.LeftButton: super().mousePressEvent(event)
self.mouse_press_pos = event.pos() return
self.mouse_press_rect = self.rect()
# 如果点击的是手柄,则开始调整大小 hit = self._hit_handle(event.pos())
if self.selected_handle: if hit == 'rotate':
event.accept() 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':
self._rotating = True
self._rotate_anchor = self.rect().center() # 旋转中心
self._last_vec = event.pos() - self._rotate_anchor # 第一帧向量
event.accept()
return
super().mousePressEvent(event)
def mouseReleaseEvent(self, event):
self._rotating = False
self._resize_corner = None
self.setCursor(Qt.ArrowCursor)
super().mouseReleaseEvent(event)
def hoverMoveEvent(self, event):
if not self.isSelected():
super().hoverMoveEvent(event)
return
hit = self._hit_handle(event.pos())
self.setCursor(Qt.PointingHandCursor if hit == 'rotate' else
Qt.SizeFDiagCursor if hit in ('topLeft', 'bottomRight') else
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):
def paint(self, painter, option, widget=None):
# 先画椭圆
painter.setPen(QPen(Qt.red, 2))
painter.setBrush(Qt.NoBrush)
painter.drawEllipse(self.rect())
# 选中时画控制手柄
if self.isSelected():
self.drawTransformHandles(painter)
class CustomGraphicsView(QGraphicsView):
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 return
super().mousePressEvent(event) super().mousePressEvent(event)
def mouseMoveEvent(self, event): def mouseMoveEvent(self, event):
"""鼠标移动事件""" if self._pan:
if event.buttons() & Qt.LeftButton and self.mouse_press_pos: delta = event.position() - self._pan_start
if self.selected_handle: self._pan_start = event.position()
# 调整大小 hs = self.horizontalScrollBar()
self.resize_item(event.pos()) vs = self.verticalScrollBar()
event.accept() hs.setValue(hs.value() - int(delta.x()))
return vs.setValue(vs.value() - int(delta.y()))
else: return
# 移动项 if self.editor.current_item and self.editor.start_pos:
super().mouseMoveEvent(event) scene_pos = self.mapToScene(event.position().toPoint())
else: rect = QRectF(self.editor.start_pos, scene_pos).normalized()
super().mouseMoveEvent(event) self.editor.current_item.setRect(rect)
return
def resize_item(self, mouse_pos): super().mouseMoveEvent(event)
"""根据鼠标位置调整项大小"""
rect = self.mouse_press_rect
pos = mouse_pos
new_rect = QRectF(rect)
# 根据选中的手柄调整矩形
if self.selected_handle == "top_left":
new_rect.setTopLeft(pos)
elif self.selected_handle == "top_right":
new_rect.setTopRight(pos)
elif self.selected_handle == "bottom_left":
new_rect.setBottomLeft(pos)
elif self.selected_handle == "bottom_right":
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):
"""鼠标释放事件""" if event.button() == Qt.MiddleButton or self._pan:
self.mouse_press_pos = None self._pan = False
self.mouse_press_rect = None self.setCursor(Qt.ArrowCursor)
self.selected_handle = None 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) super().mouseReleaseEvent(event)
def itemChange(self, change, value): def limit_view_to_image(self):
"""项变化时更新手柄位置""" """限制视图范围不超过图片边界"""
if change == QGraphicsItem.ItemSelectedChange: if not self.editor.pixmap_item:
self.update_handles() return
elif change == QGraphicsItem.ItemPositionHasChanged or change == QGraphicsItem.ItemTransformHasChanged:
self.update_handles()
return super().itemChange(change, value) # 确保滚动条不会超出范围
h_scroll = self.horizontalScrollBar()
v_scroll = self.verticalScrollBar()
class ResizableEllipseItem(ResizableGraphicsItem): h_scroll.setValue(max(0, min(h_scroll.value(), h_scroll.maximum())))
def paint(self, painter, option, widget=None): v_scroll.setValue(max(0, min(v_scroll.value(), v_scroll.maximum())))
"""绘制椭圆和手柄"""
# 绘制主椭圆
pen = QPen(Qt.red, 2, Qt.SolidLine)
if self.isSelected():
pen.setStyle(Qt.DashLine)
painter.setPen(pen)
painter.setBrush(QBrush(Qt.transparent))
painter.drawEllipse(self.rect())
# 如果选中,绘制手柄
if self.isSelected():
painter.setBrush(QBrush(Qt.white))
painter.setPen(QPen(Qt.black, 1))
for handle in self.handles.values():
painter.drawRect(handle)
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,6 +316,10 @@ 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,7 +367,23 @@ 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)
@ -259,13 +391,16 @@ class DefectMarkEditor(QDialog):
# 设置场景大小为图片大小 # 设置场景大小为图片大小
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):
"""设置为选择模式""" """设置为选择模式"""
self.mouse_state = "select" self.mouse_state = "select"
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,7 +416,7 @@ 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):
"""清除所有标记项""" """清除所有标记项"""
@ -289,86 +424,82 @@ class DefectMarkEditor(QDialog):
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): def export_annotated_image(self, output_dir):
"""场景鼠标按下事件""" """
if self.mouse_state == "select": 将当前场景原图 + 所有标记按原分辨率导出为 PNG/JPG
return 文件名与原图完全一致保存到 output_dir
"""
import os
from PySide6.QtGui import QPainter, QImage
self.start_pos = pos if not self.pixmap_item:
if self.mouse_state == "create_rect": return False
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: # 1. 取原图 QImage保持原始分辨率
self.scene.addItem(self.current_item) source_image = QImage(self.image_path)
if source_image.isNull():
return False
def mouse_move_on_scene(self, pos): # 2. 创建与原图同尺寸的 QImage 作为画布
"""场景鼠标移动事件""" out_image = QImage(source_image.size(), QImage.Format_ARGB32)
if not self.start_pos or not self.current_item: out_image.fill(Qt.transparent)
return
rect = QRectF(self.start_pos, pos).normalized() painter = QPainter(out_image)
self.current_item.setRect(rect) painter.setRenderHint(QPainter.Antialiasing)
def mouse_release_on_scene(self, pos): # 3. 先把原图完整画上去
"""场景鼠标释放事件""" painter.drawImage(0, 0, source_image)
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 # 4. 计算场景 → 原图坐标 的缩放因子
self.current_item = None scene_rect = self.pixmap_item.sceneBoundingRect()
sx = source_image.width() / scene_rect.width()
sy = source_image.height() / scene_rect.height()
# 创建完成后自动回到选择模式 # 5. 逐个绘制标记(仅最终形状,不绘制手柄)
self.set_select_mode() for item in self.scene.items():
if isinstance(item, (ResizableGraphicsItem, ResizableEllipseItem)):
# 关闭选中状态,避免手柄被绘制
was_selected = item.isSelected()
item.setSelected(False)
class CustomGraphicsView(QGraphicsView): # 计算 item 在场景中的实际几何
def __init__(self, scene, editor, parent=None): shape_path = item.mapToScene(item.shape()) # 考虑旋转
super().__init__(scene, parent) painter.setTransform(QTransform()) # 重置 painter
self.editor = editor painter.scale(sx, sy) # 映射到原图坐标
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) painter.translate(-scene_rect.x(), -scene_rect.y()) # 对齐偏移
self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
def wheelEvent(self, event): # 设置画笔
"""鼠标滚轮缩放""" pen = item.pen()
factor = 1.2 pen.setCosmetic(False) # 让线宽随缩放
if event.angleDelta().y() < 0: painter.setPen(pen)
factor = 1.0 / factor painter.setBrush(Qt.NoBrush)
self.scale(factor, factor) # 绘制形状
self.editor.scale_factor *= factor if isinstance(item, ResizableEllipseItem):
painter.drawPath(shape_path)
else:
painter.drawPath(shape_path)
def mousePressEvent(self, event): # 恢复选中状态
"""鼠标按下事件""" item.setSelected(was_selected)
if event.button() == Qt.LeftButton:
scene_pos = self.mapToScene(event.pos())
items = self.scene().items(scene_pos)
# 如果点击的是背景或图片,则开始创建 painter.end()
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): # 6. 保存文件
"""鼠标移动事件""" os.makedirs(output_dir, exist_ok=True)
if self.editor.start_pos and self.editor.current_item: base_name = os.path.basename(self.image_path)
scene_pos = self.mapToScene(event.pos()) save_path = os.path.join(output_dir, base_name)
self.editor.mouse_move_on_scene(scene_pos) return out_image.save(save_path)
else:
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event): def closeEvent(self, event):
"""鼠标释放事件""" """关闭窗口时保存标记"""
if event.button() == Qt.LeftButton and self.editor.start_pos: self.export_annotated_image("/home/dtyx/桌面/yhh/ReportGenerator/output")
scene_pos = self.mapToScene(event.pos()) super().closeEvent(event)
self.editor.mouse_release_on_scene(scene_pos)
else: if __name__ == '__main__':
super().mouseReleaseEvent(event) from PySide6.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
editor = DefectMarkEditor(image_path=r"/home/dtyx/桌面/yhh/ReportGenerator/测试数据/山东国华无棣风电场叶片外部数据/A131301/1#1-GW68.6C-I190809C/DJI_20250606162745_0005_Z.JPG")
editor.show()
sys.exit(app.exec())

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)