ReportGeneratorLocal/info_core/Img_edit.py

506 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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):
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.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):
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
# 四个角的控制点
corners = [
rect.topLeft(),
rect.topRight(),
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):
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':
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
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=""):
super().__init__(parent)
self.setWindowTitle("缺陷标记编辑器")
self.setMinimumSize(800, 600)
self.image_path = image_path
self.current_description = current_description
self.mouse_state = "select" # select | create_rect | create_ellipse
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()
def init_ui(self):
main_layout = QVBoxLayout(self)
# 工具栏
toolbar = QHBoxLayout()
self.select_btn = QPushButton("选择")
self.select_btn.setCheckable(True)
self.select_btn.setChecked(True)
self.select_btn.clicked.connect(self.set_select_mode)
self.rect_btn = QPushButton("矩形框")
self.rect_btn.setCheckable(True)
self.rect_btn.clicked.connect(self.set_create_rect_mode)
self.ellipse_btn = QPushButton("圆形框")
self.ellipse_btn.setCheckable(True)
self.ellipse_btn.clicked.connect(self.set_create_ellipse_mode)
self.clear_btn = QPushButton("清除所有标记")
self.clear_btn.clicked.connect(self.clear_all_items)
toolbar.addWidget(self.select_btn)
toolbar.addWidget(self.rect_btn)
toolbar.addWidget(self.ellipse_btn)
toolbar.addWidget(self.clear_btn)
# 场景和视图
self.scene = QGraphicsScene(self)
self.view = CustomGraphicsView(self.scene, self)
self.view.setRenderHint(QPainter.Antialiasing)
self.view.setMouseTracking(True)
self.view.setDragMode(QGraphicsView.NoDrag)
main_layout.addLayout(toolbar)
main_layout.addWidget(self.view)
def load_image(self):
"""加载图片到场景"""
if not self.image_path:
return
self.scene.clear()
# 加载图片并计算合适的缩放比例
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):
"""设置为选择模式"""
self.mouse_state = "select"
self.select_btn.setChecked(True)
self.rect_btn.setChecked(False)
self.ellipse_btn.setChecked(False)
self.view.setCursor(Qt.ArrowCursor)
def set_create_rect_mode(self):
"""设置为创建矩形模式"""
self.mouse_state = "create_rect"
self.select_btn.setChecked(False)
self.rect_btn.setChecked(True)
self.ellipse_btn.setChecked(False)
self.view.setCursor(Qt.CrossCursor)
def set_create_ellipse_mode(self):
"""设置为创建圆形模式"""
self.mouse_state = "create_ellipse"
self.select_btn.setChecked(False)
self.rect_btn.setChecked(False)
self.ellipse_btn.setChecked(True)
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 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 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/测试数据/山东国华无棣风电场叶片外部数据/A131301/1#1-GW68.6C-I190809C/DJI_20250606162745_0005_Z.JPG")
editor.show()
sys.exit(app.exec())