完成图片框选功能
This commit is contained in:
parent
cc460cfbcd
commit
f2c782a542
|
@ -1,196 +1,308 @@
|
|||
from PySide6.QtCore import Qt, QPointF, QRectF, Signal
|
||||
from PySide6.QtGui import QPixmap, QPainter, QPen, QBrush, QCursor, QColor, QPainterPath
|
||||
from PySide6.QtCore import Qt, QPointF, QRectF, Signal, QSize, QLineF
|
||||
from PySide6.QtGui import (QPixmap, QPainter, QPen, QBrush, QCursor, QColor, QImage,
|
||||
QPainterPath, QTransform)
|
||||
from PySide6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton,
|
||||
QGraphicsView, QGraphicsScene, QGraphicsItem,
|
||||
QGraphicsRectItem, QGraphicsEllipseItem,
|
||||
QGraphicsPixmapItem, QSizePolicy)
|
||||
|
||||
import math
|
||||
class ResizableGraphicsItem(QGraphicsRectItem):
|
||||
def __init__(self, x, y, width, height, parent=None):
|
||||
super().__init__(x, y, width, height, parent)
|
||||
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
|
||||
self.setFlag(QGraphicsItem.ItemIsMovable, True)
|
||||
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
|
||||
handle_radius = 12 # 手柄半径(视觉+点击区域)
|
||||
rotate_offset = 0 # 旋转手柄离矩形顶边的距离
|
||||
|
||||
def __init__(self, x, y, w, h, parent=None):
|
||||
super().__init__(x, y, w, h, parent)
|
||||
self.setFlags(QGraphicsItem.ItemIsSelectable |
|
||||
QGraphicsItem.ItemIsMovable |
|
||||
QGraphicsItem.ItemSendsGeometryChanges)
|
||||
self.setAcceptHoverEvents(True)
|
||||
|
||||
self.handle_size = 8
|
||||
self.handles = {}
|
||||
self.selected_handle = None
|
||||
self.mouse_press_pos = None
|
||||
self.mouse_press_rect = None
|
||||
|
||||
# 创建调整大小的手柄
|
||||
self.create_handles()
|
||||
|
||||
def create_handles(self):
|
||||
"""创建调整大小的手柄"""
|
||||
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):
|
||||
"""更新手柄位置"""
|
||||
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()))
|
||||
|
||||
self.setPen(QPen(Qt.blue, 2))
|
||||
self.setBrush(QBrush(Qt.transparent))
|
||||
|
||||
self.setTransformOriginPoint(self.rect().center())
|
||||
|
||||
self._rotating = False
|
||||
self._resize_corner = None
|
||||
self._orig_rect = QRectF()
|
||||
self._press_scene = QPointF()
|
||||
self._orig_rotation = 0.0
|
||||
|
||||
self._rotate_anchor = QPointF() # 旋转中心(item 坐标)
|
||||
self._last_vec = QPointF() # 上一帧鼠标相对向量
|
||||
|
||||
def setRect(self, rect):
|
||||
super().setRect(rect)
|
||||
self.setTransformOriginPoint(rect.center())
|
||||
|
||||
def paint(self, painter, option, widget=None):
|
||||
"""绘制项和手柄"""
|
||||
# 绘制主矩形
|
||||
pen = QPen(Qt.blue, 2, Qt.SolidLine)
|
||||
if self.isSelected():
|
||||
pen.setStyle(Qt.DashLine)
|
||||
painter.setPen(pen)
|
||||
painter.setBrush(QBrush(Qt.transparent))
|
||||
painter.drawRect(self.rect())
|
||||
super().paint(painter, option, widget)
|
||||
if not self.isSelected():
|
||||
return
|
||||
|
||||
painter.save()
|
||||
painter.setRenderHint(QPainter.Antialiasing) # 消除残影
|
||||
painter.setPen(QPen(Qt.black, 1))
|
||||
|
||||
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():
|
||||
painter.setBrush(QBrush(Qt.white))
|
||||
painter.setPen(QPen(Qt.black, 1))
|
||||
for handle in self.handles.values():
|
||||
painter.drawRect(handle)
|
||||
|
||||
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
|
||||
# 四个角的控制点
|
||||
corners = [
|
||||
rect.topLeft(),
|
||||
rect.topRight(),
|
||||
rect.bottomLeft(),
|
||||
rect.bottomRight()
|
||||
]
|
||||
|
||||
self.setCursor(Qt.SizeAllCursor)
|
||||
self.selected_handle = None
|
||||
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
|
||||
# 旋转控制点(顶部中间)
|
||||
rotate_pos = QPointF(rect.center().x(), rect.top() - 20)
|
||||
|
||||
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):
|
||||
"""鼠标移动事件"""
|
||||
if event.buttons() & Qt.LeftButton and self.mouse_press_pos:
|
||||
if self.selected_handle:
|
||||
# 调整大小
|
||||
self.resize_item(event.pos())
|
||||
event.accept()
|
||||
return
|
||||
else:
|
||||
# 移动项
|
||||
super().mouseMoveEvent(event)
|
||||
else:
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
def resize_item(self, mouse_pos):
|
||||
"""根据鼠标位置调整项大小"""
|
||||
rect = self.mouse_press_rect
|
||||
pos = mouse_pos
|
||||
new_rect = QRectF(rect)
|
||||
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):
|
||||
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 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()
|
||||
|
||||
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.mouse_press_pos = None
|
||||
self.mouse_press_rect = None
|
||||
self.selected_handle = None
|
||||
self._rotating = False
|
||||
self._resize_corner = None
|
||||
self.setCursor(Qt.ArrowCursor)
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
def itemChange(self, change, value):
|
||||
"""项变化时更新手柄位置"""
|
||||
if change == QGraphicsItem.ItemSelectedChange:
|
||||
self.update_handles()
|
||||
elif change == QGraphicsItem.ItemPositionHasChanged or change == QGraphicsItem.ItemTransformHasChanged:
|
||||
self.update_handles()
|
||||
|
||||
return super().itemChange(change, value)
|
||||
|
||||
|
||||
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):
|
||||
"""绘制椭圆和手柄"""
|
||||
# 绘制主椭圆
|
||||
pen = QPen(Qt.red, 2, Qt.SolidLine)
|
||||
if self.isSelected():
|
||||
pen.setStyle(Qt.DashLine)
|
||||
painter.setPen(pen)
|
||||
painter.setBrush(QBrush(Qt.transparent))
|
||||
# 先画椭圆
|
||||
painter.setPen(QPen(Qt.red, 2))
|
||||
painter.setBrush(Qt.NoBrush)
|
||||
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)
|
||||
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
|
||||
|
||||
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):
|
||||
def __init__(self, parent=None, image_path="", current_description=""):
|
||||
|
@ -204,7 +316,11 @@ class DefectMarkEditor(QDialog):
|
|||
self.start_pos = None
|
||||
self.current_item = None
|
||||
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.load_image()
|
||||
|
||||
|
@ -239,8 +355,8 @@ class DefectMarkEditor(QDialog):
|
|||
self.scene = QGraphicsScene(self)
|
||||
self.view = CustomGraphicsView(self.scene, self)
|
||||
self.view.setRenderHint(QPainter.Antialiasing)
|
||||
self.view.setDragMode(QGraphicsView.RubberBandDrag)
|
||||
self.view.setMouseTracking(True)
|
||||
self.view.setDragMode(QGraphicsView.NoDrag)
|
||||
|
||||
main_layout.addLayout(toolbar)
|
||||
main_layout.addWidget(self.view)
|
||||
|
@ -251,13 +367,32 @@ class DefectMarkEditor(QDialog):
|
|||
return
|
||||
|
||||
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.ItemIsSelectable, False)
|
||||
self.scene.addItem(self.pixmap_item)
|
||||
|
||||
# 设置场景大小为图片大小
|
||||
self.scene.setSceneRect(self.pixmap_item.boundingRect())
|
||||
|
||||
# 自动调整视图以显示整个图片
|
||||
self.view.fitInView(self.pixmap_item, Qt.KeepAspectRatio)
|
||||
|
||||
def set_select_mode(self):
|
||||
"""设置为选择模式"""
|
||||
|
@ -265,7 +400,7 @@ class DefectMarkEditor(QDialog):
|
|||
self.select_btn.setChecked(True)
|
||||
self.rect_btn.setChecked(False)
|
||||
self.ellipse_btn.setChecked(False)
|
||||
self.view.setDragMode(QGraphicsView.RubberBandDrag)
|
||||
self.view.setCursor(Qt.ArrowCursor)
|
||||
|
||||
def set_create_rect_mode(self):
|
||||
"""设置为创建矩形模式"""
|
||||
|
@ -273,7 +408,7 @@ class DefectMarkEditor(QDialog):
|
|||
self.select_btn.setChecked(False)
|
||||
self.rect_btn.setChecked(True)
|
||||
self.ellipse_btn.setChecked(False)
|
||||
self.view.setDragMode(QGraphicsView.NoDrag)
|
||||
self.view.setCursor(Qt.CrossCursor)
|
||||
|
||||
def set_create_ellipse_mode(self):
|
||||
"""设置为创建圆形模式"""
|
||||
|
@ -281,94 +416,90 @@ class DefectMarkEditor(QDialog):
|
|||
self.select_btn.setChecked(False)
|
||||
self.rect_btn.setChecked(False)
|
||||
self.ellipse_btn.setChecked(True)
|
||||
self.view.setDragMode(QGraphicsView.NoDrag)
|
||||
self.view.setCursor(Qt.CrossCursor)
|
||||
|
||||
def clear_all_items(self):
|
||||
"""清除所有标记项"""
|
||||
for item in self.scene.items():
|
||||
if isinstance(item, (ResizableGraphicsItem, ResizableEllipseItem)):
|
||||
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 __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)
|
||||
def export_annotated_image(self, output_dir):
|
||||
"""
|
||||
将当前场景(原图 + 所有标记)按原分辨率导出为 PNG/JPG,
|
||||
文件名与原图完全一致,保存到 output_dir。
|
||||
"""
|
||||
import os
|
||||
from PySide6.QtGui import QPainter, QImage
|
||||
|
||||
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):
|
||||
"""鼠标滚轮缩放"""
|
||||
factor = 1.2
|
||||
if event.angleDelta().y() < 0:
|
||||
factor = 1.0 / factor
|
||||
|
||||
self.scale(factor, factor)
|
||||
self.editor.scale_factor *= factor
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""鼠标按下事件"""
|
||||
if event.button() == Qt.LeftButton:
|
||||
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)
|
||||
def closeEvent(self, event):
|
||||
"""关闭窗口时保存标记"""
|
||||
self.export_annotated_image("/home/dtyx/桌面/yhh/ReportGenerator/output")
|
||||
super().closeEvent(event)
|
||||
|
||||
if __name__ == '__main__':
|
||||
from PySide6.QtWidgets import QApplication
|
||||
import sys
|
||||
app = QApplication(sys.argv)
|
||||
editor = DefectMarkEditor(image_path=r"/home/dtyx/桌面/yhh/ReportGenerator/测试数据/山东国华无棣风电场叶片外部数据/A1(31301)/1#(1-GW68.6C-I190809C)/DJI_20250606162745_0005_Z.JPG")
|
||||
editor.show()
|
||||
sys.exit(app.exec())
|
||||
|
|
|
@ -2586,7 +2586,7 @@ class DictionaryBrowser(QWidget):
|
|||
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:
|
||||
# 获取新描述
|
||||
new_desc = editor.get_description()
|
||||
|
@ -2648,7 +2648,7 @@ class DefectDescriptionEditor(QDialog):
|
|||
main_layout.setContentsMargins(5, 5, 5, 5)
|
||||
|
||||
# 左侧 - 图片编辑器 (使用DefectMarkEditor)
|
||||
self.image_editor = DefectMarkEditor(self, self.img_path)
|
||||
self.image_editor = DefectMarkEditor(image_path=self.img_path)
|
||||
self.image_editor.setMinimumSize(600, 600)
|
||||
main_layout.addWidget(self.image_editor, stretch=2)
|
||||
|
||||
|
|
Loading…
Reference in New Issue