欢迎关注『OpenCV-PyQT项目实战 @ Youcans』系列,持续更新中
OpenCV-PyQT项目实战(1)安装与环境配置
OpenCV-PyQT项目实战(2)QtDesigner 和 PyUIC 快速入门
OpenCV-PyQT项目实战(3)信号与槽机制
OpenCV-PyQT项目实战(4)OpenCV 与PyQt的图像转换
OpenCV-PyQT项目实战(5)项目案例01:图像模糊
OpenCV-PyQT项目实战(6)项目案例02:滚动条应用
OpenCV-PyQT项目实战(7)项目案例03:鼠标框选
OpenCV-PyQT项目实战(8)项目案例04:鼠标定位
OpenCV-PyQT项目实战(9)项目案例04:视频播放
OpenCV-PyQT项目实战(10)项目案例06:键盘事件与视频抓拍
OpenCV-PyQT项目实战(11)项目案例07:摄像头操作与拍摄视频
OpenCV-PyQT项目实战(8)项目案例03:鼠标定位
本节介绍OpenCV和PyQt 实现鼠标定位的方法和案例。通过 PyQt 创建交互界面,使用鼠标事件在图像窗口点击定位,并将鼠标定位坐标返回到 OpenCV 进行图像处理。
1. OpenCV 中实现鼠标点击定位
函数cv.setMouseCallback用于设置回调函数,将回调函数与指定窗口绑定。
函数原型:
cv. setMouseCallback (windowName, onMouse[, param]) → retval
函数cv.setMouseCallback设置回调函数,将鼠标事件响应函数onMouse与指定窗口windowName进行绑定。回调函数在鼠标事件发生时自动执行。
参数说明:
● windowName:图像显示窗口的名称
● onMouse:回调函数的函数名,鼠标事件的响应函数
● param:传递到回调函数的参数,可选项
回调函数的格式:
def onMouse (event, x, y, flags, param)
● x, y:事件发生时鼠标在图像坐标系的坐标
● param:传递到setMouseCallback函数调用的参数,可选项
● event:鼠标事件的类型
- cv.EVENT_MOUSEMOVE:鼠标移动
- cv.EVENT_LBUTTONDOWN:点击鼠标左键
- cv.EVENT_RBUTTONDOWN:点击鼠标右键
- cv.EVENT_MBUTTONDOWN:点击鼠标中键
- cv.EVENT_LBUTTONUP:释放鼠标左键
- cv.EVENT_RBUTTONUP:释放鼠标右键
- cv.EVENT_MBUTTONUP:释放鼠标中键
- cv.EVENT_LBUTTONDBLCLK:双击鼠标左键
- cv.EVENT_RBUTTONDBLCLK:双击鼠标右键
- cv.EVENT_MBUTTONDBLCLK:双击鼠标中键
● flags:查看某种按键动作是否发生
- cv.EVENT_FLAG_LBUTTON:鼠标左键拖曳
- cv.EVENT_FLAG_RBUTTON:鼠标右键拖曳
- cv.EVENT_FLAG_MBUTTON:鼠标中键拖曳
- cv.EVENT_FLAG_CTRLKEY:按下Ctrl键不放
- cv.EVENT_FLAG_SHIFTKEY:按下Shift键不放
- cv.EVENT_FLAG_ALTKEY:按下Alt键不放
注意问题:
⒈回调函数onMouse是一个通过函数指针调用的函数,是指定窗口windowName鼠标事件的响应函数,在鼠标事件发生时执行。
⒉回调函数没有返回值。需要传递变量值时,可以把变量定义为全局变量,或通过参数param进行传递。
⒊回调函数运行后会一直监听鼠标动作,相当于打开一个并行的进程,一直占用系统资源。可以使用destroyWindow函数关闭监听的窗口,回调函数就会结束。
例程8-1:OpenCV 鼠标回调函数获取点击坐标
# cvDemo08.py
# OpenCV 鼠标回调函数获取点击坐标
# Copyright 2023 Youcans, XUPT
# Crated:2023-02-16
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
def onMouseAction(event, x, y, flags, param): # 鼠标交互 (左键选点右键完成)
global status
setpoint = (x, y)
if event == cv.EVENT_LBUTTONDOWN: # 鼠标左键点击
pts.append(setpoint) # 选中一个多边形顶点
print("选择顶点 {}:{}".format(len(pts), setpoint))
elif event == cv.EVENT_MBUTTONDOWN: # 鼠标中键点击
pts.pop() # 取消最近一个顶点
elif event == cv.EVENT_RBUTTONDOWN: # 鼠标右键点击
status = False # 结束绘图状态
print("结束绘制,按 ESC 退出。")
if __name__ == '__main__':
img = cv.imread("../images/imgLena.tif") # 读取彩色图像(BGR)
imgCopy = img.copy()
# 鼠标交互 ROI
print("单击左键:选择 ROI 顶点")
print("单击中键:删除最近的顶点")
print("单击右键:结束 ROI 选择")
print("按 ESC 退出")
pts = [] # 初始化 ROI 顶点坐标集合
status = True # 开始绘图状态
cv.namedWindow('origin') # 创建图像显示窗口
cv.setMouseCallback('origin', onMouseAction, status) # 绑定回调函数
while True:
if len(pts) > 0:
cv.circle(imgCopy, pts[-1], 5, (0,0,255), -1) # 绘制最近一个顶点
if len(pts) > 1:
cv.line(imgCopy, pts[-1], pts[-2], (255, 0, 0), 2) # 绘制最近一段线段
if status == False: # 判断结束绘制 ROI
cv.line(imgCopy, pts[0], pts[-1], (255,0,0), 2) # 绘制最后一段线段
cv.imshow('origin', imgCopy)
key = 0xFF & cv.waitKey(10) # 按 ESC 退出
if key == 27: # Esc 退出
break
cv.destroyAllWindows() # 释放图像窗口
# 提取多边形 ROI
print("ROI 顶点坐标:", pts)
points = np.array(pts, np.int) # ROI 多边形顶点坐标集
cv.polylines(img, [points], True, (255,255,255), 2) # 在 img 绘制 ROI 多边形
mask = np.zeros(img.shape[:2], np.uint8) # 黑色掩模,单通道
cv.fillPoly(mask, [points], (255,255,255)) # 多边形 ROI 为白色窗口
imgROI = cv.bitwise_and(img, img, mask=mask) # 按位与,从 img 中提取 ROI
plt.figure(figsize=(9, 6))
plt.subplot(131), plt.title("origin image"), plt.axis('off')
plt.imshow(cv.cvtColor(img, cv.COLOR_BGR2RGB))
plt.subplot(132), plt.title("ROI mask"), plt.axis('off')
plt.imshow(mask, cmap='gray')
plt.subplot(133), plt.title("ROI cropped"), plt.axis('off')
plt.imshow(cv.cvtColor(imgROI, cv.COLOR_BGR2RGB))
plt.tight_layout()
plt.show()
2. PyQt实现鼠标点击定位
PyQt 中实现鼠标点击定位,本质上是鼠标动作的响应。
2.1 支持鼠标事件的自定义 Label 类
基本的 QLabel 类并不支持鼠标动作,因此需要自定义一个支持鼠标动作的 Label 类。
PyQt中,每个事件类型都被封装成相应的事件类,如鼠标事件为QMouseEvent,键盘事件为QKeyEvent等。而它们的基类是QEvent。
QMouseEvent 鼠标事件:
mousePressEvent (self, event):鼠标按下事件
mouseReleaseEvent (self, event):鼠标释放事件
mouseDoubieCiickEvent (self, event):双击鼠标事件
mouseMoveEvent(self,event):鼠标移动事件
enterEvent (self, event):鼠标进入控件事件
leaveEvent (self, event):鼠标离开控件事件
wheelEvent (self, event):滚轮滚动事件
QMouseEvent 鼠标方法:
ignore():让父控件继续收到鼠标事件
accept():不让父控件继续收到鼠标事件
x()、y():返回相对于控件空间的鼠标坐标值
pos():返回相对于控件空间的QPoint对象
localPos():返回相对于控件空间的QPointF对象
globalX()、globalY():返回相对于屏幕的x,y 坐标值
globalPos():返回相对于屏幕的QPoint对象
windowPos():返回相对于窗口的QPointF对象
screenPos():返回相对于屏幕的QPointF对象
timestamp():返回事件发生的时间;
QMouseEvent 鼠标事件的具体内容:
按下并释放鼠标按钮时,将调用以下方法:
mousePressEvent (self, event) - 鼠标键按下时调用;
mouseReleaseEvent (self, event) - 鼠标键公开时调用;
mouseDoubieCiickEvent (self, event) - 双击鼠标时调用。必须注意,在双击之前的其他事件。双击时的事件顺序如下:
- MouseButtonPress
- MouseButtonRelease
- MouseButtonDblClick
- MouseButtonPress
- MouseButtonRelease
2.2 例程7-2:支持鼠标事件的自定义 MyLabel 类
程序说明:
- 在图像窗口点击鼠标左键,选取 2 个基准点绘制直线;点击鼠标右键,清除选择的基准点;
- 超过 2个基准点时,自动清零,重新选取基准点;
- 将选取点的坐标传回主程序,进行图像处理。
class MyLabel(QLabel):
def __init__(self, parent=None):
super(MyLabel, self).__init__(parent)
self.points = []
def paintEvent(self, event):
super().paintEvent(event)
painter = QPainter()
painter.begin(self)
painter.setPen(QPen(Qt.blue, 4, Qt.SolidLine)) # 实线画笔,蓝色
if len(self.points)==2:
painter.drawLine(self.points[0][0], self.points[0][1], self.points[1][0], self.points[1][1])
painter.setPen(QPen(Qt.red, 10)) # 画点,红色
for k in range(len(self.points)):
painter.drawPoint(self.points[k][0], self.points[k][1])
painter.end()
def mousePressEvent(self, event):
if event.buttons() == Qt.LeftButton: # 左键点击
if len(self.points)==2:
self.points.clear() # 清空绝对坐标
x0 = event.x()
y0 = event.y()
self.points.append([x0, y0])
self.update() # 获取鼠标点击的点之后,通知画线
elif event.buttons() == Qt.RightButton: # 右键点击
self.points.clear() # 清空绝对坐标
print("Clear selected points!")
def getGlobalPos(self): # 返回绝对坐标
return self.points
3. 项目实战:PyQt 鼠标点击定位
本项目基于 PyQt5 GUI,使用鼠标在图像窗口区域点击选取两点,绘制直线,以该直线为水平基准线,旋转图像并显示在第二窗口。
3.1 使用 QtDesigner 开发 PyQt5 图形界面
本例的 UI 继承自 uiDemo4.ui :
于是,我们就完成了本项目的图形界面设计,将其保存为 uiDemo9.ui文件。
在 PyCharm中,使用 PyUIC 将选中的 uiDemo9.ui 文件转换为 .py 文件,就得到了 uiDemo9.py 文件。
3.2. 项目主程序的开发
3.2.1 实例化 MyLabel 类
自定义的 MyLabel 类不能在 QtDesigner 中创建,要在主程序中定义如下。
self.label_1 = MyLabel(self.centralwidget)
self.label_1.setGeometry(QRect(20, 20, 400, 320))
self.label_1.setAlignment(Qt.AlignCenter)
self.label_1.setObjectName("label_1")
3.2.2 图像旋转槽函数
click_pushButton槽函数,由 pushButton_3.clicked 按钮信号触发。
def click_pushButton_3(self): # 点击 pushButton_3 触发 旋转图像
print("pushButton_3")
height, width = self.img1.shape[:2] # 图片的高度和宽度
pointList = self.label_1.getGlobalPos() # 获取坐标点集
if len(pointList) < 2:
print("Points number < 2")
return
else:
pts = np.array(pointList) # 转为 Numpy
print(pts)
# 计算倾斜角 angle
radian = np.arctan((pts[1][1] - pts[0][1]) / (pts[1][0] - pts[0][0])) # arctan((y2-y1)/(x2-x1))
angle = radian * 180 / 3.1415926 # 角度制
print(pts, angle)
x0, y0 = width//2, height//2 # 以图像中心作为旋转中心
MARot = cv.getRotationMatrix2D((x0, y0), angle, 1.0) # 计算旋转变换矩阵
self.img2 = cv.warpAffine(self.img1, MARot, (height, width)) # 旋转图像
self.refreshShow(self.img2, self.label_2) # 刷新显示
return
3.2.3 信号与槽的连接
# 通过 connect 建立信号/槽连接,点击按钮事件发射 triggered 信号,执行相应的子程序 click_pushButton
self.pushButton_1.clicked.connect(self.click_pushButton_1) # 按钮触发:导入图像
self.pushButton_2.clicked.connect(self.click_pushButton_2) # # 按钮触发:灰度显示
self.pushButton_3.clicked.connect(self.click_pushButton_3) # # 按钮触发:旋转图像
self.pushButton_4.clicked.connect(self.trigger_actHelp) # # 按钮触发
self.pushButton_5.clicked.connect(self.close) # 点击 # 按钮触发:关闭
3.3 完整例程 OpenCVPyqt09.py
# OpenCVPyqt09.py
# Demo07 of GUI by PyQt5
# Copyright 2023 Youcans, XUPT
# Crated:2023-02-16
import sys
import cv2 as cv
import numpy as np
from PyQt5.QtCore import QObject, pyqtSignal, QPoint, QRect, qDebug, Qt
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from uiDemo9 import Ui_MainWindow # 导入 uiDemo8.py 中的 Ui_MainWindow 界面类
class MyLabel(QLabel):
def __init__(self, parent=None):
super(MyLabel, self).__init__(parent)
self.points = []
def paintEvent(self, event):
super().paintEvent(event)
painter = QPainter()
painter.begin(self)
painter.setPen(QPen(Qt.blue, 4, Qt.SolidLine)) # 实线画笔,蓝色
if len(self.points)==2:
painter.drawLine(self.points[0][0], self.points[0][1], self.points[1][0], self.points[1][1])
painter.setPen(QPen(Qt.red, 10)) # 画点,红色
for k in range(len(self.points)):
painter.drawPoint(self.points[k][0], self.points[k][1])
painter.end()
def mousePressEvent(self, event):
if event.buttons() == Qt.LeftButton: # 左键点击
if len(self.points)==2:
self.points.clear() # 清空绝对坐标
x0 = event.x()
y0 = event.y()
self.points.append([x0, y0])
self.update() # 获取鼠标点击的点之后,通知画线
elif event.buttons() == Qt.RightButton: # 右键点击
self.points.clear() # 清空绝对坐标
print("Clear selected points!")
def getGlobalPos(self): # 返回绝对坐标
return self.points
class MyMainWindow(QMainWindow, Ui_MainWindow): # 继承 QMainWindow 类和 Ui_MainWindow 界面类
def __init__(self, parent=None):
super(MyMainWindow, self).__init__(parent) # 初始化父类
self.setupUi(self) # 继承 Ui_MainWindow 界面类
self.label_1 = MyLabel(self.centralwidget)
self.label_1.setGeometry(QRect(20, 20, 400, 320))
self.label_1.setAlignment(Qt.AlignCenter)
self.label_1.setObjectName("label_1")
# 菜单栏
self.actionOpen.triggered.connect(self.openSlot) # 连接并执行 openSlot 子程序
self.actionSave.triggered.connect(self.saveSlot) # 连接并执行 saveSlot 子程序
self.actionHelp.triggered.connect(self.trigger_actHelp) # 连接并执行 trigger_actHelp 子程序
self.actionQuit.triggered.connect(self.close) # 连接并执行 trigger_actHelp 子程序
# 通过 connect 建立信号/槽连接,点击按钮事件发射 triggered 信号,执行相应的子程序 click_pushButton
self.pushButton_1.clicked.connect(self.click_pushButton_1) # 按钮触发:导入图像
self.pushButton_2.clicked.connect(self.click_pushButton_2) # # 按钮触发:灰度显示
self.pushButton_3.clicked.connect(self.click_pushButton_3) # # 按钮触发:旋转图像
self.pushButton_4.clicked.connect(self.trigger_actHelp) # # 按钮触发
self.pushButton_5.clicked.connect(self.close) # 点击 # 按钮触发:关闭
# 初始化
self.img1 = np.ndarray(()) # 初始化图像 ndarry,用于存储图像
self.img2 = np.ndarray(()) # 初始化图像 ndarry,用于存储图像
self.img1 = cv.imread("../images/Lena.tif") # OpenCV 读取图像
self.refreshShow(self.img1, self.label_1)
# self.refreshShow(self.img1, self.label_2)
return
def click_pushButton_1(self): # 点击 pushButton_1 触发
self.img1 = self.openSlot() # 读取图像
print("click_pushButton_1", self.img1.shape)
self.refreshShow(self.img1, self.label_1) # 刷新显示
return
def click_pushButton_2(self): # 点击 pushButton_2 触发
print("pushButton_2")
self.img2 = self.img1.copy()
self.img2 = cv.cvtColor(self.img2, cv.COLOR_BGR2GRAY) # 图片格式转换:BGR -> Gray
self.refreshShow(self.img2, self.label_2) # 刷新显示
return
def click_pushButton_3(self): # 点击 pushButton_3 触发 旋转图像
print("pushButton_3")
height, width = self.img1.shape[:2] # 图片的高度和宽度
pointList = self.label_1.getGlobalPos() # 获取坐标点集
if len(pointList) < 2:
print("Points number < 2")
return
else:
pts = np.array(pointList) # 转为 Numpy
print(pts)
# 计算倾斜角 angle
radian = np.arctan((pts[1][1] - pts[0][1]) / (pts[1][0] - pts[0][0])) # arctan((y2-y1)/(x2-x1))
angle = radian * 180 / 3.1415926 # 角度制
print(pts, angle)
x0, y0 = width//2, height//2 # 以图像中心作为旋转中心
MARot = cv.getRotationMatrix2D((x0, y0), angle, 1.0) # 计算旋转变换矩阵
self.img2 = cv.warpAffine(self.img1, MARot, (height, width)) # 旋转图像
self.refreshShow(self.img2, self.label_2) # 刷新显示
return
def refreshShow(self, img, label):
print(img.shape, label)
qImg = self.cvToQImage(img) # OpenCV 转为 PyQt 图像格式
# label.setScaledContents(False) # 需要在图片显示之前进行设置
label.setPixmap((QPixmap.fromImage(qImg))) # 加载 PyQt 图像
return
def openSlot(self, flag=1): # 读取图像文件
# OpenCV 读取图像文件
fileName, _ = QFileDialog.getOpenFileName(self, "Open Image", "../images/", "*.png *.jpg *.tif")
if flag==0 or flag=="gray":
img = cv.imread(fileName, cv.IMREAD_GRAYSCALE) # 读取灰度图像
else:
img = cv.imread(fileName, cv.IMREAD_COLOR) # 读取彩色图像
print(fileName, img.shape)
return img
def saveSlot(self): # 保存图像文件
# 选择存储文件 dialog
fileName, tmp = QFileDialog.getSaveFileName(self, "Save Image", "../images/", '*.png; *.jpg; *.tif')
if self.img1.size == 1:
return
# OpenCV 写入图像文件
ret = cv.imwrite(fileName, self.img1)
if ret:
print(fileName, self.img.shape)
return
def cvToQImage(self, image):
# 8-bits unsigned, NO. OF CHANNELS=1
if image.dtype == np.uint8:
channels = 1 if len(image.shape) == 2 else image.shape[2]
if channels == 3: # CV_8UC3
# Create QImage with same dimensions as input Mat
qImg = QImage(image, image.shape[1], image.shape[0], image.strides[0], QImage.Format_RGB888)
return qImg.rgbSwapped()
elif channels == 1:
# Create QImage with same dimensions as input Mat
qImg = QImage(image, image.shape[1], image.shape[0], image.strides[0], QImage.Format_Indexed8)
return qImg
else:
qDebug("ERROR: numpy.ndarray could not be converted to QImage. Channels = %d" % image.shape[2])
return QImage()
def qPixmapToCV(self, qPixmap): # PyQt图像 转换为 OpenCV图像
qImg = qPixmap.toImage() # QPixmap 转换为 QImage
shape = (qImg.height(), qImg.bytesPerLine() * 8 // qImg.depth())
shape += (4,)
ptr = qImg.bits()
ptr.setsize(qImg.byteCount())
image = np.array(ptr, dtype=np.uint8).reshape(shape) # 定义 OpenCV 图像
image = image[..., :3]
return image
def trigger_actHelp(self): # 动作 actHelp 触发
QMessageBox.about(self, "About",
"""数字图像处理工具箱 v1.0\nCopyright YouCans, XUPT 2023""")
return
if __name__ == '__main__':
app = QApplication(sys.argv) # 在 QApplication 方法中使用,创建应用程序对象
myWin = MyMainWindow() # 实例化 MyMainWindow 类,创建主窗口
myWin.show() # 在桌面显示控件 myWin
sys.exit(app.exec_()) # 结束进程,退出程序
运行结果:
【本节完】
版权声明:
Copyright 2023 youcans, XUPT文章来源:https://www.toymoban.com/news/detail-496670.html
Crated:2023-2-16文章来源地址https://www.toymoban.com/news/detail-496670.html
到了这里,关于OpenCV-PyQT项目实战(8)项目案例03:鼠标定位的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!