QLabel 用鼠标画选框 并显示框在图片上,并保留等比例缩放后图片的原始坐标 用pyqt实现
时间: 2025-07-26 13:25:40 浏览: 6
<think>我们将在QLabel上实现鼠标绘制选框,并处理图片缩放后的坐标转换。需求:1.在QLabel上显示图片,并允许用户通过鼠标拖拽绘制矩形框。2.图片可能被缩放(等比例),但矩形框的坐标需要转换为原始图片的坐标。3.绘制过程中,矩形框应实时显示;释放鼠标后,记录矩形框在原始图片中的坐标。实现思路:1.创建一个自定义的QLabel(例如ImageLabel),重写鼠标事件和绘制事件。2.在鼠标按下、移动、释放时记录坐标,并在paintEvent中绘制矩形。3.由于图片在QLabel中可能缩放显示,我们需要记录原始图片和显示图片的尺寸,以便坐标转换。步骤:a.在ImageLabel中,存储原始图片(QPixmap)和缩放后的图片(用于显示)。b.计算显示图片时,图片在QLabel中的实际显示区域(因为QLabel可能设置了缩放并保持宽高比,图片不一定填满整个QLabel)。c.鼠标事件中获取的坐标需要转换到原始图片坐标。坐标转换:显示区域(scaled_rect):图片在QLabel中实际显示的区域(居中显示时,周围可能有空白)。将鼠标在QLabel上的坐标(x, y)转换为在原始图片上的坐标:x_in_scaled =x- scaled_rect.x()y_in_scaled =y -scaled_rect.y()然后按比例转换:x_original= (x_in_scaled/scaled_rect.width()) *original_widthy_original =(y_in_scaled /scaled_rect.height()) *original_height注意:在绘制矩形框时,我们是在显示区域上绘制,所以需要将原始坐标转换为显示区域的坐标(用于绘制),但存储的矩形框坐标应该是原始坐标。具体实现:1.自定义ImageLabel类,继承QLabel。2.添加以下属性:-start_point:鼠标按下的起始点(在原始图片坐标)- end_point:鼠标释放的结束点(在原始图片坐标)-current_point:鼠标移动过程中的当前点(用于实时绘制)-drawing:是否正在绘制- original_pixmap:原始图片-scaled_pixmap:缩放后的图片(根据QLabel大小缩放)-scaled_rect:缩放后图片在QLabel中的显示区域(用于坐标转换)3.重写方法:-setPixmap:设置原始图片,同时计算缩放后的图片和显示区域。- resizeEvent:当窗口大小改变时,重新计算缩放图片和显示区域。- paintEvent:除了显示图片,还要绘制矩形框(如果正在绘制或已经绘制)。-鼠标事件:mousePressEvent, mouseMoveEvent, mouseReleaseEvent。4.坐标转换函数:-将QLabel坐标转换为原始图片坐标:labelPosToOriginal()-将原始图片坐标转换为QLabel中显示区域的坐标:originalPosToLabel()5.绘制矩形框:在paintEvent中,使用QPainter绘制矩形。注意矩形框的坐标和尺寸需要在显示区域上绘制,所以使用转换后的显示坐标。注意:为了保持原始坐标,我们存储的矩形框坐标(start_point,end_point)都是原始图片坐标。代码结构示例:
```pythonfrom PyQt5.QtWidgetsimport QLabel,QApplicationfromPyQt5.QtGuiimport QPixmap,QPainter,QPen, QImagefrom PyQt5.QtCore importQt,QRect, QPoint,QSizeclassImageLabel(QLabel):def__init__(self, parent=None):super().__init__(parent)self.original_pixmap= None#原始图片self.scaled_pixmap= None#缩放后的图片self.scaled_rect =None#缩放图片在Label中的显示区域(包括空白边)self.start_point =None#矩形框起始点(原始图片坐标)self.end_point =None#矩形框结束点(原始图片坐标)self.current_point =None#当前鼠标位置(用于实时绘制)self.drawing= False#是否正在绘制self.setMouseTracking(True)#启用鼠标跟踪self.setAlignment(Qt.AlignCenter)#居中显示def setPixmap(self, pixmap):#保存原始图片self.original_pixmap= pixmap#初始缩放self.rescaleImage(self.size())super().setPixmap(self.scaled_pixmap)defrescaleImage(self, size):"""根据给定的尺寸(QLabel的大小)缩放图片,并保持宽高比"""if self.original_pixmap isNone orself.original_pixmap.isNull():return#计算缩放后的图片大小,保持宽高比scaled_pixmap= self.original_pixmap.scaled(size, Qt.KeepAspectRatio, Qt.SmoothTransformation)self.scaled_pixmap= scaled_pixmap#计算缩放后图片在QLabel中的显示区域(居中显示)label_size= sizew,h =scaled_pixmap.width(),scaled_pixmap.height()x= (label_size.width()- w) //2y =(label_size.height() -h)//2self.scaled_rect= QRect(x, y, w, h)def resizeEvent(self, event):"""窗口大小改变时,重新缩放图片"""super().resizeEvent(event)ifself.original_pixmapand notself.original_pixmap.isNull():self.rescaleImage(event.size())super().setPixmap(self.scaled_pixmap)def labelPosToOriginal(self, point):"""将QLabel上的坐标点转换为原始图片上的坐标"""ifnot self.scaled_rect:returnNone#如果点在显示区域外,返回Noneifnot self.scaled_rect.contains(point):return None#相对于显示区域的坐标x_in_scaled= point.x()- self.scaled_rect.x()y_in_scaled =point.y() -self.scaled_rect.y()#转换为原始图片坐标original_width= self.original_pixmap.width()original_height= self.original_pixmap.height()scaled_width= self.scaled_rect.width()scaled_height =self.scaled_rect.height()#注意:缩放后的图片尺寸可能为0,需要避免除以0ifscaled_width==0 orscaled_height==0:returnNonex_original= (x_in_scaled/scaled_width) *original_widthy_original =(y_in_scaled /scaled_height) *original_heightreturn QPoint(int(x_original),int(y_original))def originalPosToLabel(self, point):"""将原始图片坐标转换为QLabel上的坐标(在显示区域内)"""ifnot self.scaled_rector notself.original_pixmap:return Noneoriginal_width= self.original_pixmap.width()original_height= self.original_pixmap.height()if original_width ==0ororiginal_height==0:returnNone#在显示区域内的比例x_ratio =point.x() /original_widthy_ratio =point.y() /original_height#转换为显示区域内的坐标x_label= self.scaled_rect.x()+ int(x_ratio *self.scaled_rect.width())y_label= self.scaled_rect.y()+ int(y_ratio *self.scaled_rect.height())return QPoint(x_label, y_label)defpaintEvent(self,event):"""绘制事件:先绘制图片,再绘制矩形框"""super().paintEvent(event)#如果有绘制中的矩形框,则绘制ifself.drawing andself.start_point andself.current_point:#将原始坐标转换为QLabel上的坐标start_label =self.originalPosToLabel(self.start_point)current_label =self.originalPosToLabel(self.current_point)ifnot start_label ornot current_label:return#绘制矩形框painter =QPainter(self)painter.setPen(QPen(Qt.red,2,Qt.SolidLine))rect =QRect(start_label, current_label).normalized()painter.drawRect(rect)#如果已经绘制完成,也可以绘制存储的矩形框(根据需求)elifself.start_pointand self.end_point:start_label= self.originalPosToLabel(self.start_point)end_label= self.originalPosToLabel(self.end_point)if notstart_labelor notend_label:returnpainter= QPainter(self)painter.setPen(QPen(Qt.red,2, Qt.SolidLine))rect= QRect(start_label,end_label).normalized()painter.drawRect(rect)def mousePressEvent(self,event):ifevent.button() ==Qt.LeftButtonand self.scaled_rectand self.scaled_rect.contains(event.pos()):#转换为原始坐标point= self.labelPosToOriginal(event.pos())if point:self.drawing =Trueself.start_point= pointself.current_point =pointself.update()#触发重绘super().mousePressEvent(event)defmouseMoveEvent(self, event):if self.drawingand self.scaled_rectand self.scaled_rect.contains(event.pos()):point =self.labelPosToOriginal(event.pos())ifpoint:self.current_point= pointself.update()#实时更新矩形框super().mouseMoveEvent(event)def mouseReleaseEvent(self,event):ifevent.button() ==Qt.LeftButtonand self.drawing:ifself.scaled_rect andself.scaled_rect.contains(event.pos()):point= self.labelPosToOriginal(event.pos())if point:self.end_point =pointself.current_point =Noneself.drawing= Falseself.update()#这里可以输出矩形框的原始坐标rect= QRect(self.start_point, self.end_point).normalized()print(f"原始坐标矩形框: x={rect.x()}, y={rect.y()}, width={rect.width()}, height={rect.height()}")else:#如果鼠标释放在显示区域外,则取消绘制self.drawing =Falseself.start_point= Noneself.end_point =Noneself.current_point= Noneself.update()super().mouseReleaseEvent(event)
```使用示例:```pythonimportsysfrom PyQt5.QtWidgetsimport QApplication,QMainWindow,QVBoxLayout, QWidgetclass MainWindow(QMainWindow):def __init__(self):super().__init__()self.setWindowTitle("图片标注工具")self.setGeometry(100,100,800,600)central_widget =QWidget()self.setCentralWidget(central_widget)layout =QVBoxLayout(central_widget)self.image_label= ImageLabel()layout.addWidget(self.image_label)#加载图片pixmap= QPixmap("image.jpg")#替换为你的图片路径ifnotpixmap.isNull():self.image_label.setPixmap(pixmap)else:self.image_label.setText("图片加载失败")if __name__== "__main__":app= QApplication(sys.argv)window= MainWindow()window.show()sys.exit(app.exec_())```注意事项:1.缩放时使用Qt.SmoothTransformation保证质量。2.坐标转换时注意整数坐标的舍入。3.矩形框绘制在QLabel上,所以当图片缩放时,矩形框也会随之缩放(因为我们每次绘制时都进行坐标转换)。这样,无论图片如何缩放,我们存储的矩形框坐标都是原始图片的坐标,并且绘制时也能正确显示在缩放后的图片上。引用:参考了在QLabel上显示图片并绘制矩形框的思路[^1],以及坐标转换的方法[^2]。</think># PyQt5 QLabel 实现鼠标绘制选框(支持缩放和原始坐标)
我将为您实现一个在QLabel上显示图片,并通过鼠标绘制矩形选框的功能。这个实现支持图片等比例缩放,并能正确转换坐标到原始图片坐标系。
```python
import sys
from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel, QVBoxLayout,
QWidget, QPushButton, QHBoxLayout, QMessageBox)
from PyQt5.QtGui import QPixmap, QPainter, QPen, QImage
from PyQt5.QtCore import Qt, QRect, QPoint, QSize
class ImageLabel(QLabel):
def __init__(self, parent=None):
super().__init__(parent)
self.setMouseTracking(True)
self.setAlignment(Qt.AlignCenter)
self.setStyleSheet("border: 1px solid #cccccc; background-color: #f0f0f0;")
# 图片相关属性
self.original_pixmap = None
self.scaled_pixmap = None
self.scaled_rect = None # 缩放后图片在QLabel中的位置
# 选框相关属性
self.start_point = None
self.current_point = None
self.drawing = False
self.selection_rect = None
# 原始坐标选框
self.original_selection = None
def set_image(self, file_path):
"""设置图片并保持原始图片"""
self.original_pixmap = QPixmap(file_path)
if self.original_pixmap.isNull():
QMessageBox.warning(self, "错误", "无法加载图片")
return False
self.scale_image()
return True
def scale_image(self):
"""缩放图片以适应QLabel大小"""
if not self.original_pixmap:
return
label_size = self.size()
# 计算保持宽高比的缩放尺寸
scaled = self.original_pixmap.scaled(
label_size,
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
self.scaled_pixmap = scaled
self.setPixmap(scaled)
# 计算缩放后图片在QLabel中的位置(居中)
w = scaled.width()
h = scaled.height()
x = (label_size.width() - w) // 2
y = (label_size.height() - h) // 2
self.scaled_rect = QRect(x, y, w, h)
# 重置选框
self.start_point = None
self.current_point = None
self.selection_rect = None
self.original_selection = None
def resizeEvent(self, event):
"""窗口大小改变时重新缩放图片"""
super().resizeEvent(event)
self.scale_image()
def paintEvent(self, event):
"""重绘事件,绘制图片和选框"""
super().paintEvent(event)
# 绘制选框
if self.drawing and self.start_point and self.current_point:
painter = QPainter(self)
painter.setPen(QPen(Qt.blue, 2, Qt.DashLine))
rect = QRect(self.start_point, self.current_point).normalized()
painter.drawRect(rect)
# 绘制已完成的选框
elif self.selection_rect:
painter = QPainter(self)
painter.setPen(QPen(Qt.red, 2, Qt.SolidLine))
painter.drawRect(self.selection_rect)
def mousePressEvent(self, event):
"""鼠标按下事件"""
if event.button() == Qt.LeftButton and self.scaled_rect and self.scaled_rect.contains(event.pos()):
self.drawing = True
self.start_point = event.pos()
self.current_point = event.pos()
self.update()
def mouseMoveEvent(self, event):
"""鼠标移动事件"""
if self.drawing and self.scaled_rect and self.scaled_rect.contains(event.pos()):
self.current_point = event.pos()
self.update()
elif self.scaled_rect:
# 显示当前坐标
if self.scaled_rect.contains(event.pos()):
orig_point = self.label_to_original(event.pos())
self.setToolTip(f"原始坐标: ({orig_point.x()}, {orig_point.y()})")
else:
self.setToolTip("")
def mouseReleaseEvent(self, event):
"""鼠标释放事件"""
if event.button() == Qt.LeftButton and self.drawing:
self.drawing = False
if self.scaled_rect and self.scaled_rect.contains(event.pos()):
self.current_point = event.pos()
self.selection_rect = QRect(self.start_point, self.current_point).normalized()
# 转换为原始图片坐标
top_left = self.label_to_original(self.selection_rect.topLeft())
bottom_right = self.label_to_original(self.selection_rect.bottomRight())
self.original_selection = QRect(top_left, bottom_right)
print(f"选框原始坐标: x={top_left.x()}, y={top_left.y()}, "
f"width={self.original_selection.width()}, height={self.original_selection.height()}")
else:
# 如果鼠标释放在图片区域外,取消选框
self.start_point = None
self.current_point = None
self.selection_rect = None
self.update()
def label_to_original(self, label_point):
"""将QLabel坐标转换为原始图片坐标"""
if not self.original_pixmap or not self.scaled_rect:
return QPoint()
# 计算缩放比例
scale_x = self.original_pixmap.width() / self.scaled_rect.width()
scale_y = self.original_pixmap.height() / self.scaled_rect.height()
# 转换为相对于图片左上角的坐标
rel_x = label_point.x() - self.scaled_rect.x()
rel_y = label_point.y() - self.scaled_rect.y()
# 转换为原始图片坐标
orig_x = int(rel_x * scale_x)
orig_y = int(rel_y * scale_y)
# 确保坐标在图片范围内
orig_x = max(0, min(orig_x, self.original_pixmap.width() - 1))
orig_y = max(0, min(orig_y, self.original_pixmap.height() - 1))
return QPoint(orig_x, orig_y)
def clear_selection(self):
"""清除选框"""
self.start_point = None
self.current_point = None
self.selection_rect = None
self.original_selection = None
self.update()
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("QLabel图片选框工具")
self.setGeometry(100, 100, 800, 600)
# 创建主部件和布局
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
# 创建图片标签
self.image_label = ImageLabel()
main_layout.addWidget(self.image_label, 1)
# 创建按钮区域
button_layout = QHBoxLayout()
load_button = QPushButton("加载图片")
load_button.clicked.connect(self.load_image)
button_layout.addWidget(load_button)
clear_button = QPushButton("清除选框")
clear_button.clicked.connect(self.image_label.clear_selection)
button_layout.addWidget(clear_button)
info_button = QPushButton("选框信息")
info_button.clicked.connect(self.show_selection_info)
button_layout.addWidget(info_button)
main_layout.addLayout(button_layout)
# 加载默认图片
self.load_image("example.jpg") # 替换为您的图片路径
def load_image(self, file_path=None):
"""加载图片"""
if not file_path:
# 在实际应用中,这里应该打开文件对话框
file_path = "example.jpg" # 替换为您的图片路径
if self.image_label.set_image(file_path):
self.setWindowTitle(f"QLabel图片选框工具 - {file_path}")
def show_selection_info(self):
"""显示选框信息"""
if not self.image_label.original_selection:
QMessageBox.information(self, "信息", "当前没有选框")
return
rect = self.image_label.original_selection
info = (f"原始图片选框信息:\n"
f"左上角: ({rect.x()}, {rect.y()})\n"
f"宽度: {rect.width()} 像素\n"
f"高度: {rect.height()} 像素\n"
f"右下角: ({rect.right()}, {rect.bottom()})")
QMessageBox.information(self, "选框信息", info)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
```
## 功能说明
1. **图片显示与缩放**:
- 图片自动等比例缩放以适应QLabel大小
- 保持原始图片尺寸信息
- 居中显示,周围有灰色边框
2. **选框绘制**:
- 鼠标在图片区域按下并拖动可绘制蓝色虚线选框
- 释放鼠标后选框变为红色实线
- 支持清除选框功能
3. **坐标转换**:
- 将QLabel上的坐标转换为原始图片坐标
- 保存选框在原始图片中的位置和尺寸
- 鼠标移动时显示当前坐标对应的原始图片坐标
4. **用户界面**:
- 提供加载图片、清除选框和显示选框信息按钮
- 状态提示和错误处理
## 实现原理
1. **坐标转换**:
```python
def label_to_original(self, label_point):
# 计算缩放比例
scale_x = self.original_pixmap.width() / self.scaled_rect.width()
scale_y = self.original_pixmap.height() / self.scaled_rect.height()
# 转换为相对于图片左上角的坐标
rel_x = label_point.x() - self.scaled_rect.x()
rel_y = label_point.y() - self.scaled_rect.y()
# 转换为原始图片坐标
orig_x = int(rel_x * scale_x)
orig_y = int(rel_y * scale_y)
```
2. **选框绘制**:
- 在`paintEvent`中根据绘图状态绘制不同样式的矩形
- 鼠标事件处理绘制过程的开始、移动和结束
3. **图片缩放**:
- 使用`QPixmap.scaled()`保持宽高比
- 在`resizeEvent`中处理窗口大小变化时的重新缩放
## 使用方法
1. 运行程序后会自动加载默认图片
2. 在图片区域按下鼠标左键并拖动绘制选框
3. 释放鼠标后选框会固定
4. 使用"清除选框"按钮移除当前选框
5. 使用"选框信息"按钮查看选框在原始图片中的坐标
这个实现考虑了图片缩放后的坐标转换问题,确保选框位置和尺寸在原始图片坐标系中是准确的[^1][^2]。
阅读全文
相关推荐


















