import os
import sys
from PyQt5.QtWidgets import (
QApplication, # 应用主类,管理事件循环
QMainWindow, # 主窗口框架(带菜单栏/状态栏)
QWidget, # 基础空白控件(所有可视组件的基类)
QVBoxLayout, # 垂直布局管理器
QHBoxLayout, # 水平布局管理器
QPushButton, # 按钮控件
QLabel, # 文本/图片显示标签
QFileDialog, # 文件选择对话框
QMessageBox # 消息提示框
)
from PyQt5.QtGui import (
QPixmap, # 图像显示载体(优化显示的图像类)
QPainter, # 绘图工具(可在控件上绘制图形)
QPen, # 画笔(设置线条颜色/粗细)
QImage, # 图像数据容器(像素级操作)
QColor
)
from PyQt5.QtCore import (
Qt, # 包含枚举常量(如对齐方式、鼠标按键)
QPoint, # 二维坐标点(x,y)
QRect # 矩形区域(x,y,width,height)
)
import requests
class OCRImage(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("图片标注工具")
self.setGeometry(100, 100, 800, 600)
# 初始化变量
self.image_path = None
self.start_point = QPoint() # 绘制方框时的起始点和结束点
self.end_point = QPoint() # 绘制方框时的起始点和结束点
self.drawing = False
self.rectangles = [] # 存储所有方框
# 初始化UI
self.init_ui()
def init_ui(self):
# 主窗口部件
central_widget = QWidget() # 它将作为主窗口的中心部件,用于容纳其他控件。
self.setCentralWidget(central_widget) # 将 central_widget 设置为当前主窗口的中心部件。在 QMainWindow 中,中心部件是用来显示主要内容的区域。
main_layout = QVBoxLayout(central_widget) # 创建了一个垂直布局管理器 main_layout,并将其应用到 central_widget 上。这意味着后续添加到 central_widget 中的子控件会按照垂直方向依次排列。
# 按钮区域
btn_layout = QHBoxLayout() # 创建了一个水平布局管理器 btn_layout,用于管理按钮的布局。后续添加到这个布局中的按钮会按照水平方向依次排列。
self.btn_open = QPushButton("打开图片") # 创建按钮
self.btn_open.clicked.connect(self.open_image) # 绑定方法
btn_layout.addWidget(self.btn_open) # 将按钮添加到水平布局 btn_layout 中。
self.btn_upload = QPushButton("上传图片")
self.btn_upload.setEnabled(False)
self.btn_upload.clicked.connect(self.upload_image)
btn_layout.addWidget(self.btn_upload)
self.btn_clear = QPushButton("清除方框")
self.btn_clear.setEnabled(False)
self.btn_clear.clicked.connect(self.clear_rectangles)
btn_layout.addWidget(self.btn_clear)
main_layout.addLayout(btn_layout) # 将水平布局 btn_layout 添加到垂直布局 main_layout 中。
# 图片显示区域
self.lbl_image = QLabel() # 创建一个 QLabel 类的实例 self.lbl_image。QLabel 是 PyQt5 中用于显示文本或图像的控件。在这个应用场景中,它将用于显示图片。
self.lbl_image.setAlignment(Qt.AlignCenter) # 设置 QLabel 内内容的对齐方式为居中对齐。Qt.AlignCenter 是 PyQt5 中定义的一个常量,表示居中对齐。这样,当在 QLabel 中显示图片时,图片会在 QLabel 区域内居中显示。
self.lbl_image.setStyleSheet("background-color: #333;") # 设置样式表。这里设置了 QLabel 的背景颜色为 #333(深灰色)。样式表是一种用于设置控件外观的机制,类似于 CSS 在网页中的作用。
self.lbl_image.mousePressEvent = self.mouse_press # 鼠标按下事件
self.lbl_image.mouseMoveEvent = self.mouse_move # 鼠标移动事件
self.lbl_image.mouseReleaseEvent = self.mouse_release # 鼠标释放事件
main_layout.addWidget(self.lbl_image) # 将QLabel 添加到垂直布局 main_layout 中。
def open_image(self):
""" 打开图片 """
file_path, _ = QFileDialog.getOpenFileName( # 用于弹出文件选择对话框,让用户选择一个文件。
self, # 表示对话框的父窗口,这里通常是当前的主窗口。
"选择图片", # 说明文字
"D:\Desktop", # 对话框打开时默认显示的路径
"图片文件 (*.jpg *.jpeg *.png)" # 文件过滤器,用于限制用户只能选择指定类型的文件,这里只允许选择 JPEG 和 PNG 格式的图片文件。
)
if file_path:
self.image_path = file_path
self.rectangles = [] # 清除之前的方框
self.load_image()
self.btn_upload.setEnabled(True)
self.btn_clear.setEnabled(True)
def load_image(self):
""" 加载图片,展示 """
self.pixmap = QPixmap(self.image_path) # 加载各种格式的图片文件。
self.scaled_pixmap = self.pixmap.scaled( # 用于对图片进行缩放。
self.lbl_image.size(), # 表示 QLabel 控件的当前大小,即图片将被缩放到适合 QLabel 控件的大小。
Qt.KeepAspectRatio, # 表示在缩放图片时保持图片的原始宽高比,避免图片变形。
Qt.SmoothTransformation # 是一个转换模式的标志,表示使用平滑的转换算法进行缩放,这样可以使缩放后的图片更加清晰。
)
self.lbl_image.setPixmap(self.scaled_pixmap) # 将指定的 QPixmap 对象显示在 QLabel 控件上。
self.update()
def mouse_press(self, event):
""" 鼠标点击事件 """
# 处理鼠标按下事件,当用户按下鼠标左键且已经加载了图片时,开始绘制矩形框。
if event.button() == Qt.LeftButton and self.image_path:
self.drawing = True # 标记开始绘制矩形框。
self.start_point = event.pos() # 将矩形框的起始点和结束点都设置为当前鼠标位置。
self.end_point = event.pos()
def mouse_move(self, event):
""" 鼠标移动事件 """
if self.drawing and self.image_path:
self.end_point = event.pos()
self.update_image_with_rect()
def mouse_release(self, event):
if event.button() == Qt.LeftButton and self.drawing:
self.drawing = False
# 将方框添加到列表
rect = QRect(self.start_point, self.end_point).normalized()
self.rectangles.append(rect)
self.update_image_with_rect()
def update_image_with_rect(self):
""" 主要功能是在图片上绘制矩形框(标注),并将绘制好的图片显示在 QLabel 控件上。 """
# 创建临时图片用于绘制
temp_pixmap = self.scaled_pixmap.copy() # 创建副本,避免直接在原始图片上进行绘制,从而保持原始图片的完整性。
painter = QPainter(temp_pixmap) # 创建一个 QPainter 对象 painter,用于在 temp_pixmap 上进行绘制操作。
painter.setPen(QPen(QColor(148, 0, 211), 2, Qt.SolidLine)) # 设置绘制矩形框时使用的画笔。Qt.red 表示画笔的颜色为红色,2 表示画笔的宽度为 2 像素,Qt.SolidLine 表示画笔的样式为实线。
# 绘制所有已保存的方框
for rect in self.rectangles:
painter.drawRect(rect)
# 绘制当前正在画的方框
if self.drawing:
current_rect = QRect(self.start_point, self.end_point).normalized() # 如果正在绘制矩形框,根据当前的起始点 self.start_point 和结束点 self.end_point 创建一个 QRect 对象 current_rect,并使用 normalized 方法确保矩形框的宽度和高度为正值。
painter.drawRect(current_rect) # 在 temp_pixmap 上绘制当前正在绘制的矩形框。
painter.end() # 结束 QPainter 的绘制操作,释放相关资源。
self.lbl_image.setPixmap(temp_pixmap) # 将绘制好的 temp_pixmap 设置为 self.lbl_image 的显示内容,从而更新界面上显示的图片。
def clear_rectangles(self):
self.rectangles = []
self.load_image() # 重新加载原始图片
def upload_image(self):
if not self.image_path:
return
# 1. 在原图上绘制方框
original_image = QImage(self.image_path) # 依据 self.image_path 加载原始图片。
painter = QPainter(original_image) # 创建 QPainter 对象,用于在原始图片上绘制标注框。
painter.setPen(QPen(QColor(148, 0, 211), 5, Qt.SolidLine)) # 设置画笔,颜色为红色,宽度为 5 像素,样式为实线。
# 计算方框在原始图片上的位置(考虑缩放比例)
scale_x = self.pixmap.width() / self.scaled_pixmap.width()
scale_y = self.pixmap.height() / self.scaled_pixmap.height()
for rect in self.rectangles:
# 转换坐标到原始图片尺寸
original_rect = QRect(
int(rect.x() * scale_x),
int(rect.y() * scale_y),
int(rect.width() * scale_x),
int(rect.height() * scale_y)
)
painter.drawRect(original_rect) # 绘制到原始图片上。
painter.end() # 结束绘制操作。
# 2. 保存临时文件
curr_path = __file__ # 当前文件路径
base_dir = os.path.dirname(os.path.dirname(curr_path))
temp_path = os.path.join(base_dir, 'media', 'temp', 'img', self.image_path.split("/")[-1])
original_image.save(temp_path)
# 3. 模拟上传(替换为你的真实API)
try:
# 示例:使用requests上传
url = "http://127.0.0.1:8000/api/ocr/img/"
files = {'plane': open(temp_path, 'rb')}
response = requests.post(url, files=files)
QMessageBox.information(
self,
"上传成功",
f"文件已上传!\n服务器返回: {response.text}"
)
except Exception as e:
QMessageBox.critical(
self,
"上传失败",
f"上传出错: {str(e)}"
)
def resizeEvent(self, event):
if hasattr(self, 'pixmap') and self.pixmap:
self.load_image() # 窗口大小改变时重新缩放图片