震惊!!一男子用尽了各种方式都搜不到这个资源,于是他竟然将手伸向了......!?pyqt pyside 随窗口自适应、可缩放、拖动QLabel
需求场景
在小才玩yolo目标检测时,总是使用PyQt或PySide来展示识别结果。一开始小才使用自带的QLabel来显示图像,但是发现显示的比较呆,原QLabel显示有以下特点:
1. 窗口大小改变时,QLabel图片显示不会跟随窗口变化。
2. 图片不能够缩放,拖动。
于是小才就去搜索学习,许久后,总结加拓展自己实现了一个 随窗口自适应、可缩放、拖动、画框图片的自定义QLabel类(嘲笑请偷偷)。
实现效果:(温馨提示:代码部分较多,最底部领取无套路、免费打包好项目,直接运行!)
实现
不废话直接上代码
import cv2
from PySide6.QtCore import QPoint, Qt, QRect
from PySide6.QtCore import Signal
from PySide6.QtGui import QPainter, QPixmap, QImage, QMouseEvent, QKeyEvent, QPen, QColor
from PySide6.QtWidgets import QLabel
from check_except import check_except
class ImageLabel(QLabel):
clicked_signal = Signal()
# clicked_signal = pyqtSignal()# pyqt5
# 当画框完成后向外输出信号 index: 0原图 1 裁剪后的图片 [2-5裁剪范围]2:x1 3:y1 4:width 5:height
bbox_down_signal = Signal(list)
def __init__(self, parent, scale_flag=True, move_flag=True, is_original=False, draw_bbox_flag=False):
super(ImageLabel, self).__init__(parent)
'''
@param parent: 父控件
@param scale_flag: 是否可以缩放
@param move_flag: 是否可以移动
@param is_original: 是否为原图
@param draw_bbox_flag: 是否开启画框
'''
self.scaled_img = None # QPixmap
self.cv_image = None # draw_bbox_flag=True 时,为cv2原图
self.point = None # 图片位置
self.start_pos = None # 鼠标按下时的位置
self.end_pos = None # 鼠标松开时的位置
self.right_press = False # 右键正在按压
self.left_press = False # 左键正在按压
self.is_original = is_original # 是否为原图显示
self.x_scale = 1
self.y_scale = 1
# 画框起始点
self.bbox_start_point = None
# 画框结束点
self.bbox_end_point = None
# 是否可以缩放 scale_flag True 可以缩放
self.scale_flag = scale_flag
# 是否可以移动
self.move_flag = move_flag
# 是否开启画框
self.draw_bbox_flag = draw_bbox_flag
@check_except()
def clear_image(self):
self.set_q_pixmap(QPixmap(None))
@check_except()
def to_q_image(self, image):
label_size = (self.height(), self.width())
height, width = image.shape[:2]
if not self.is_original:
n_height = label_size[0]
n_width = int((width / height) * label_size[0])
if n_width > label_size[1]:
n_width = label_size[1]
n_height = int((height / width) * label_size[1])
image = cv2.resize(image, (n_width, n_height))
else:
n_height = height
n_width = width
channels = len(image.shape)
if channels == 2:
bytes_per_line = n_width
q_image = QImage(image.data, n_width, n_height, bytes_per_line, QImage.Format_Grayscale8)
else:
bytes_per_line = 3 * n_width
q_image = QImage(image.data, n_width, n_height, bytes_per_line, QImage.Format_RGB888).rgbSwapped()
return q_image
@check_except()
def set_cv_image(self, img):
self.set_q_image(self.to_q_image(img))
@check_except()
def set_q_image(self, img):
self.set_q_pixmap(QPixmap.fromImage(img))
@check_except()
def set_q_pixmap(self, q_pixmap):
self.scaled_img = q_pixmap
if self.point is None:
self._reset_scale()
self._reset_point()
self.update()
def _reset_scale(self):
if self.scaled_img:
self.x_scale = self.width() / self.scaled_img.width()
b = self.scaled_img.height() / self.scaled_img.width()
n_height = self.width() * b
if n_height > self.height():
self.y_scale = self.height() / self.scaled_img.height()
self.x_scale = self.y_scale
else:
self.y_scale = self.x_scale
self.reset_bbox()
def _reset_point(self):
if self.scaled_img:
width, height = self.scaled_img.width(), self.scaled_img.height()
p1 = 0
p2 = 0
if self.width() > width * self.x_scale:
p1 = int((self.width() - width * self.x_scale) / 2)
if self.height() > height * self.y_scale:
p2 = int((self.height() - height * self.x_scale) / 2)
self.point = QPoint(int(p1 / self.x_scale) if self.x_scale != 0 else 0,
int(p2 / self.y_scale) if self.y_scale != 0 else 0)
# 重置框
self.reset_bbox()
def reset_bbox(self):
self.bbox_start_point = None
self.bbox_end_point = None
@check_except()
def paintEvent(self, e):
if self.scaled_img:
painter = QPainter()
painter.begin(self)
painter.scale(self.x_scale, self.y_scale)
painter.drawPixmap(self.point, self.scaled_img) # 此函数中还会用scale对point进行处理
if self.draw_bbox_flag and self.bbox_start_point is not None and self.bbox_end_point is not None:
lw = max(round(sum(self.cv_image.shape) / 2 * 0.003), 3)
fs = lw / 3 # line width
pen = QPen(QColor('red'))
pen.setWidth(fs)
painter.setPen(pen)
painter.drawRect(self.bbox_start_point.x(), self.bbox_start_point.y(),
self.bbox_end_point.x() - self.bbox_start_point.x(),
self.bbox_end_point.y() - self.bbox_start_point.y())
painter.end()
@check_except()
def clear_image(self):
self.scaled_img = None
self.update()
@check_except()
def wheelEvent(self, event):
if self.scale_flag:
angle = event.angleDelta() / 8 # 返回QPoint对象,为滚轮转过的数值,单位为1/8度
angleY = angle.y()
self.wheel_flag = True
# 获取当前鼠标相对于view的位置
if angleY > 0:
self.set_scale(x_scale=1.08 * self.x_scale, y_scale=1.08 * self.y_scale)
else: # 滚轮下滚
self.set_scale(x_scale=0.92 * self.x_scale, y_scale=0.92 * self.y_scale)
if self.x_scale < 0.05:
self.x_scale = 0.05
if self.y_scale < 0.05:
self.y_scale = 0.05
if self.x_scale > 50:
self.x_scale = 50
if self.y_scale > 50:
self.y_scale = 50
self.update()
def set_scale(self, x_scale=None, y_scale=None):
if x_scale is not None:
self.x_scale = x_scale
if y_scale is not None:
self.y_scale = y_scale
@check_except()
def mouseMoveEvent(self, e):
if self.scaled_img is None:
return
if self.right_press and self.move_flag:
self.end_pos: QPoint = e.pos() - self.start_pos # 当前位置-起始位置=差值
x = round(self.end_pos.x() / self.x_scale)
y = round(self.end_pos.y() / self.y_scale)
self.point = self.point + QPoint(x, y) # 左上角的距离变化
if self.bbox_start_point is not None:
self.bbox_end_point = self.bbox_end_point + QPoint(x, y)
if self.bbox_start_point is not None:
self.bbox_start_point = self.bbox_start_point + QPoint(x, y)
self.start_pos = e.pos()
self.repaint()
elif self.left_press and self.draw_bbox_flag:
img_rect = self.get_image_rect()
if img_rect is not None:
current_press_point = QPoint(int(e.x() / self.x_scale), int(e.y() / self.y_scale))
if img_rect.contains(current_press_point):
# 设置框的结束位置点
self.bbox_end_point = current_press_point
self.repaint()
@check_except()
def mousePressEvent(self, e):
try:
if e.button() == Qt.RightButton:
self.right_press = True
if self.move_flag:
self.start_pos = e.pos()
elif e.button() == Qt.LeftButton:
self.left_press = True
if self.draw_bbox_flag:
img_rect = self.get_image_rect()
if img_rect is not None:
current_press_point = QPoint(int(e.x() / self.x_scale), int(e.y() / self.y_scale))
if img_rect.contains(current_press_point):
# 设置框的起始点和结束点
self.bbox_start_point = current_press_point
self.bbox_end_point = current_press_point
except Exception as e:
print("mousePressEvent error", e)
def get_image_rect(self):
if self.point is not None and self.scaled_img is not None:
return QRect(self.point, self.get_image_end_point())
def get_image_end_point(self):
if self.point is not None and self.scaled_img is not None:
return self.point + QPoint(self.scaled_img.width(), self.scaled_img.height())
@check_except()
def mouseReleaseEvent(self, e: QMouseEvent):
if e.button() == Qt.RightButton and self.move_flag:
self.right_press = False
elif e.button() == Qt.LeftButton:
self.clicked_signal.emit()
@check_except()
# 监听按键事件
def keyReleaseEvent_out(self, event: QKeyEvent):
if self.draw_bbox_flag:
# enter键 发送坐标点
if event.key() == Qt.Key_Enter or event.key() == Qt.Key_Return:
if self.bbox_start_point is not None and self.bbox_end_point is not None:
# 获取左上角点坐标
bbox_top_left_point, bbox_bottom_right_point = self.get_bbox_top_left_and_bottom_right_point()
# 计算左上角点比例
d_x, d_y = (bbox_top_left_point.x() - self.point.x()) / self.scaled_img.width(), (
bbox_top_left_point.y() - self.point.y()) / self.scaled_img.height()
diff_x = bbox_bottom_right_point.x() - bbox_top_left_point.x()
diff_y = bbox_bottom_right_point.y() - bbox_top_left_point.y()
if diff_x <= 0 or diff_y <= 0:
return
# 计算宽高比例
d_w, d_h = (bbox_bottom_right_point.x() - bbox_top_left_point.x()) / self.scaled_img.width(), (
bbox_bottom_right_point.y() - bbox_top_left_point.y()) / self.scaled_img.height()
# 原始cv图片宽高
width, height = self.cv_image.shape[1], self.cv_image.shape[0]
# 计算原始图片top_left点
top_left_point = int(d_x * width), int(d_y * height)
# 计算原始图片宽高
width, height = int(d_w * width), int(d_h * height)
# 裁剪cv图片
cropped_image = self.cv_image[top_left_point[1]:top_left_point[1] + height,
top_left_point[0]:top_left_point[0] + width].copy()
self.bbox_down_signal.emit([self.cv_image, cropped_image, top_left_point, width, height])
print("bbox_down_signal emit")
# delete键 清除框
elif event.key() == Qt.Key_Delete or event.key() == Qt.Key_Backspace:
self.reset_bbox()
self.repaint()
print("reset_bbox")
def get_bbox_top_left_and_bottom_right_point(self):
if self.bbox_start_point is not None and self.bbox_end_point is not None:
return (QPoint(min(self.bbox_start_point.x(), self.bbox_end_point.x()),
min(self.bbox_start_point.y(), self.bbox_end_point.y())),
QPoint(max(self.bbox_start_point.x(), self.bbox_end_point.x()),
max(self.bbox_start_point.y(), self.bbox_end_point.y())))
def resizeEvent(self, event):
# 图片随着窗口大小而改变
self._reset_scale()
self._reset_point()
# 图片放大
def zoom_in(self):
self.x_scale *= 1.2
self.y_scale *= 1.2
self.update()
# 图片缩小
def zoom_out(self):
self.x_scale *= 0.8
self.y_scale *= 0.8
self.update()
def reset_all(self):
self.reset_scale()
self.reset_point()
def cropOnAndOff(self):
self.draw_bbox_flag = not self.draw_bbox_flag
def reset_scale(self):
self._reset_scale()
self.update()
def reset_point(self):
self._reset_point()
self.update()
以上代码是使用的pyside6实现,pyqt和pyside导入的都差不多,如果使用PyQt5的话删除上面pyside6的import,重新使用pycharm的自动导入提示重新import就行。
ImageLabel继承的QLabel。因此使用时直接将写好的label = QLabel(parent)
替换为label = ImageLabel(parent)
就可以得到升级!方法上的装饰器check_except
是为了方便异常捕捉,小才不想因为显示报错而导致页面卡住然后退出。实现请看小才得另一篇文章python
程序运行异常与计算耗时@装饰器,不想看的话注释掉@check_except()
即可。
功能和使用
代码写好了,有时候小才找资源急眼的时候根本就不看原理,直接cv代码拿来用,下次再去琢磨,嗯,能用就行,下次一定琢磨。
1.参数设置
@param parent: 父控件
@param scale_flag: 是否可以缩放 默认可以
@param move_flag: 是否可以移动 默认可以
@param is_original: 是否为原图 默认不是
@param draw_bbox_flag: 是否开启画框 默认不开启
2.设置图片
使ImageLabel显示图片有三种方式如下:
@check_except()
def set_cv_image(self, img): # 设置cv2读取的图片
self.set_q_image(self.to_q_image(img))
@check_except()
def set_q_image(self, img): # 设置QImage图片
self.set_q_pixmap(QPixmap.fromImage(img))
@check_except()
def set_q_pixmap(self, q_pixmap): # 设置QPixmap图片
self.scaled_img = q_pixmap
if self.point is None:
self._reset_scale()
self._reset_point()
self.update()
3.缩放
通过鼠标滚轮旋转控制,ImageLabel中还提供了放大、缩小、复原至label大小的方法,分别为zoom_in()
、zoom_out()
、reset_all()
。
4.拖动
按住鼠标右键进行拖动
5.小惊喜(裁剪图片)
按需使用。当draw_bbox_flag设置为True时,可以在图片上进行BBOX框绘制,单击回车键会通过bbox_down_signal信号
向外传当前裁剪的图片信息列表,注意!使用前,设置显示图片的同时需要设置对应的imagelLabel.cv_image = 显示的cv2图片
。列表位置元素为:
- 0号位: cv_image原图
- 1号位:BBOX裁剪的cv图片
- 2-3号位:裁剪图片左上角在原图中的位置
- 4-5:裁剪图片宽高
def bbox_down_callback(bbox_point_list):
cv_image, cropped_image, top_left_point, width, height = bbox_point_list
# 裁剪使用案例
label = ImageLabel(parent,draw_bbox_flag=True)
# bbox绘制完并按下回车键时会回调bbox_down_callback
label .bbox_down_signal.connect(bbox_down_callback)
......
# 需要在QMainWindow主界面的按键事件中调用ImageLabel的keyReleaseEvent_out
def KeyPressEvent(self, event):
self.label.keyReleaseEvent_out(event)
完整使用案例
使用的是pyside6,pyqt5的控件类名一致的
1.使用Qt Designer设计一个简单界面
ui文件:
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QWidget" name="widget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>1</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>50</height>
</size>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="pushButton">
<property name="text">
<string>打开图片</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_2">
<property name="text">
<string>放大</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_3">
<property name="text">
<string>缩小</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_4">
<property name="text">
<string>复原</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="widget_2" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>20</horstretch>
<verstretch>20</verstretch>
</sizepolicy>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>
转换为py文件:
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'test_custom_label.ui'
##
## Created by: Qt User Interface Compiler version 6.5.3
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QGridLayout, QHBoxLayout, QLabel,
QMainWindow, QPushButton, QSizePolicy, QSpacerItem,
QVBoxLayout, QWidget)
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
if not MainWindow.objectName():
MainWindow.setObjectName(u"MainWindow")
MainWindow.resize(800, 600)
self.centralwidget = QWidget(MainWindow)
self.centralwidget.setObjectName(u"centralwidget")
self.verticalLayout = QVBoxLayout(self.centralwidget)
self.verticalLayout.setObjectName(u"verticalLayout")
self.widget = QWidget(self.centralwidget)
self.widget.setObjectName(u"widget")
sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(1)
sizePolicy.setVerticalStretch(1)
sizePolicy.setHeightForWidth(self.widget.sizePolicy().hasHeightForWidth())
self.widget.setSizePolicy(sizePolicy)
self.widget.setMinimumSize(QSize(0, 50))
self.widget.setMaximumSize(QSize(16777215, 50))
self.horizontalLayout = QHBoxLayout(self.widget)
self.horizontalLayout.setObjectName(u"horizontalLayout")
self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
self.horizontalLayout.addItem(self.horizontalSpacer)
self.pushButton = QPushButton(self.widget)
self.pushButton.setObjectName(u"pushButton")
self.horizontalLayout.addWidget(self.pushButton)
self.pushButton_2 = QPushButton(self.widget)
self.pushButton_2.setObjectName(u"pushButton_2")
self.horizontalLayout.addWidget(self.pushButton_2)
self.pushButton_3 = QPushButton(self.widget)
self.pushButton_3.setObjectName(u"pushButton_3")
self.horizontalLayout.addWidget(self.pushButton_3)
self.pushButton_4 = QPushButton(self.widget)
self.pushButton_4.setObjectName(u"pushButton_4")
self.horizontalLayout.addWidget(self.pushButton_4)
self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
self.horizontalLayout.addItem(self.horizontalSpacer_2)
self.verticalLayout.addWidget(self.widget)
self.widget_2 = QWidget(self.centralwidget)
self.widget_2.setObjectName(u"widget_2")
sizePolicy1 = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
sizePolicy1.setHorizontalStretch(20)
sizePolicy1.setVerticalStretch(20)
sizePolicy1.setHeightForWidth(self.widget_2.sizePolicy().hasHeightForWidth())
self.widget_2.setSizePolicy(sizePolicy1)
self.gridLayout = QGridLayout(self.widget_2)
self.gridLayout.setObjectName(u"gridLayout")
self.label = QLabel(self.widget_2)
self.label.setObjectName(u"label")
self.gridLayout.addWidget(self.label, 0, 0, 1, 1)
self.verticalLayout.addWidget(self.widget_2)
MainWindow.setCentralWidget(self.centralwidget)
self.retranslateUi(MainWindow)
QMetaObject.connectSlotsByName(MainWindow)
# setupUi
def retranslateUi(self, MainWindow):
MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None))
self.pushButton.setText(QCoreApplication.translate("MainWindow", u"\u6253\u5f00\u56fe\u7247", None))
self.pushButton_2.setText(QCoreApplication.translate("MainWindow", u"\u653e\u5927", None))
self.pushButton_3.setText(QCoreApplication.translate("MainWindow", u"\u7f29\u5c0f", None))
self.pushButton_4.setText(QCoreApplication.translate("MainWindow", u"\u590d\u539f", None))
self.label.setText("")
# retranslateUi
修改装换后的label为ImageLabel,即替换self.label = QLabel(self.widget_2)
为self.label = ImageLabel(self.widget_2, )
2.引用制作的界面
import sys
import cv2
from PySide6.QtWidgets import QMainWindow, QApplication, QFileDialog
from test_custom_labelUI import Ui_MainWindow
IMG_EXTENSIONS = (".bmp", ".jpg", ".jpeg", ".png", ".ppm", ".pgm", ".tif", ".tiff", ".webp")
def bbox_down_callback(bbox_point_list):
cv_image, cropped_image, top_left_point, width, height = bbox_point_list
cv2.namedWindow("cropped_image")
cv2.imshow("cropped_image", cropped_image)
cv2.waitKey(1)
class MainUI(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super(MainUI, self).__init__(parent)
# 初始化UI
self.setupUi(self)
# 初始化按键点击槽函数
# 打开图片
self.pushButton.clicked.connect(self.open_image)
# 放大
self.pushButton_2.clicked.connect(self.label.zoom_in)
# 缩小
self.pushButton_3.clicked.connect(self.label.zoom_out)
# 复原
self.pushButton_4.clicked.connect(self.label.reset_all)
# 裁剪
self.label.bbox_down_signal.connect(bbox_down_callback)
def open_image(self):
fileName, fileType = QFileDialog.getOpenFileName(self, f"请选择图片", ".",
" ".join(list(map(lambda x: "*" + x, IMG_EXTENSIONS))))
if fileName == "":
# 取消选择则退出
return
img = cv2.imread(fileName)
self.label.set_cv_image(img)
self.label.cv_image = img
def keyReleaseEvent(self, event):
self.label.keyReleaseEvent_out(event)
if __name__ == '__main__':
app = QApplication(sys.argv)
win = MainUI()
win.show()
sys.exit(app.exec())
“不要忘记关注我,点赞收藏,我会为你带来更多优质内容!”
“你的关注是我前进的动力,你的点赞是我创作的源泉。”
“点个赞吧!让我知道你在支持我,我会继续努力的!”
“关注我,点赞收藏,让我有更多的动力去创作更好的内容!”
“你的每一个点赞,都是我创作的动力,感谢你的关注和支持!”
“希望你喜欢我的内容,记得关注我哦!我会继续为大家带来更好的作品!”
资源获取 感谢您的支持和鼓励! 😊🙏
如果大家对相关文章感兴趣,可以搜索并关注公众号"人才兄呐",查看和领取打包资源,完全免费哒!无套路直接领取。如果资源不存在可以直接联系小才我给你私发!更多有用资源持续手动更新中~~
“点赞+评论,让我知道你在关注我,感谢每一个支持我的人!”