我的问题类似于如何在 Qml 应用程序中显示 OpenCV 相机源?但我正在尝试在 QML/PySide6 应用程序中显示 OpenCV 相机源。这是一个非常简单的应用程序,有两个按钮(一个用于开始流式传输,一个用于停止)和一个用于显示视频的图像元素。
我能够分配静态图像,但我的问题是如何动态地将发出的图像传递给requestImage,每帧分配一个新图像?
另外,在QML文件中,onImageChanged函数将图像作为属性,但我无法理解如何将其与
source: "image://MyImageProvider/img"
连接
我找不到其他 openCV 和 QML/PySide6 集成示例,我感谢任何帮助。
这是我正在使用的代码:
main.py
import sys
from pathlib import Path
import os
import cv2
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtGui import QIcon, QPixmap, QImage
from PySide6.QtWidgets import QFileDialog, QApplication
from PySide6.QtCore import Qt, QThread, Signal, Slot, QObject, QSize
from PySide6.QtQuick import QQuickPaintedItem, QQuickView, QQuickImageProvider
class ThreadCamera(QThread):
updateFrame = Signal(QImage)
def __init__(self, parent=None):
QThread.__init__(self, parent)
def run(self):
self.cap = cv2.VideoCapture(0)
while self.cap.isOpened():
ret, frame = self.cap.read()
if not ret:
img = QImage("./images/network.png")
self.updateFrame.emit(img)
color_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
img = QImage(color_frame.data, color_frame.shape[1], color_frame.shape[0], QImage.Format_RGB888)
self.updateFrame.emit(img)
class ImageProvider(QQuickImageProvider):
imageChanged = Signal(QImage)
def __init__(self):
super(ImageProvider, self).__init__(QQuickImageProvider.Image)
self.cam = ThreadCamera()
self.cam.updateFrame.connect(self.update_image)
def requestImage(self, id, size, requestedSize):
img = QImage(600, 500, QImage.Format_RGBA8888)
img.fill(Qt.black)
return img
@Slot()
def update_image(self, img):
self.imageChanged.emit(img)
@Slot()
def start(self):
print("Starting...")
self.cam.start()
@Slot()
def killThread(self):
print("Finishing...")
try:
self.cam.cap.release()
cv2.destroyAllWindows()
except:
pass
class MainWindow(QObject):
def __init__(self):
QObject.__init__(self)
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setWindowIcon(QIcon("./images/network.png"));
engine = QQmlApplicationEngine()
#Get Context
main = MainWindow()
myImageProvider = ImageProvider()
engine.rootContext().setContextProperty("backend", main)
engine.rootContext().setContextProperty("myImageProvider", myImageProvider)
engine.addImageProvider("MyImageProvider", myImageProvider)
#Load QML File
engine.load(os.fspath(Path(__file__).resolve().parent / "test2.qml"))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec())
main.qml
import QtQuick 2.15
import QtQuick.Window
import QtMultimedia
import QtQuick.Controls 6.3
import QtQuick.Layouts 6.3
import QtQuick.Dialogs
Window {
visible: true
width: 600
height: 500
title: "WebCam"
Image {
id: feedImage
width: parent.width
height: parent.height - 50
fillMode: Image.PreserveAspectFit
cache: false
source: "image://MyImageProvider/img"
property bool counter: false
function reloadImage() {
counter = !counter
source = "image://MyImageProvider/img?id=" + counter
}
}
RowLayout {
anchors.top: feedImage.bottom
anchors.horizontalCenter: feedImage.horizontalCenter
Button {
id: btnStartCamera
text: "Start Camera"
onClicked: {
myImageProvider.start()
}
}
Button {
id: btnStopCamera
text: "Stop Camera"
onClicked: {
myImageProvider.killThread()
}
}
}
Connections{
target: myImageProvider
function onImageChanged(image) {
console.log("emit")
feedImage.reloadImage()
}
}
}
我在 QTWidgets 方面有更多经验,并且刚刚开始学习 QML!
更新
我设法通过更改 ImageProvider 类来显示视频流,如下所示:
class ImageProvider(QQuickImageProvider):
imageChanged = Signal(QImage)
def __init__(self):
super(ImageProvider, self).__init__(QQuickImageProvider.Image)
self.cam = ThreadCamera()
self.cam.updateFrame.connect(self.update_image)
self.image = None
def requestImage(self, id, size, requestedSize):
if self.image:
img = self.image
else:
img = QImage(600, 500, QImage.Format_RGBA8888)
img.fill(Qt.black)
return img
@Slot()
def update_image(self, img):
self.imageChanged.emit(img)
self.image = img
@Slot()
def start(self):
print("Starting...")
self.cam.start()
@Slot()
def killThread(self):
print("Finishing...")
try:
self.cam.cap.release()
cv2.destroyAllWindows()
except:
pass
不知道这是否是正确的方法,但效果很好。
这些代码对我帮助很大,如下所示。我使用 QML 编写一个 GUI,其输入可以是相机、视频或图像目录。我可以成功运行它,所以我认为这可能对某人有帮助,我在下面的最后发布了我的代码。
# those codes help me a lot
myImageProvider = ImageProvider()
engine.rootContext().setContextProperty("backend", main)
engine.rootContext().setContextProperty("myImageProvider", myImageProvider)
engine.addImageProvider("MyImageProvider", myImageProvider)
........
Connections{
target: myImageProvider
function onImageChanged(image) {
console.log("emit")
feedImage.reloadImage()
}
}
我写的代码可以成功运行。检测、分割、姿势检测。人类等等。
python main.py
main.py
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial
import sys
from pathlib import Path
from PySide6.QtCore import QObject, Slot
from PySide6.QtGui import QGuiApplication, QAction, QImage, QKeySequence, QPixmap
from PySide6.QtQml import QQmlApplicationEngine, QmlElement, QQmlImageProviderBase
from PySide6.QtQuickControls2 import QQuickStyle
from PySide6.QtQuick import QQuickView, QQuickImageProvider
from PySide6.QtCore import QUrl, QThread, Signal, QStandardPaths, Property, QSize
from PySide6.QtWidgets import QDialog, QMainWindow, QMessageBox, QFileDialog, QInputDialog
import cv2
from datetime import datetime
from queue import Queue
from ultralytics import YOLO
import os
import time
import numpy as np
from PySide6.QtMultimedia import QMediaFormat
# pyside6-uic camera_video.ui -o ui_camera_video.py
AVI = "video/x-msvideo" # AVI
MP4 = 'video/mp4'
def get_supported_mime_types():
result = []
for f in QMediaFormat().supportedFileFormats(QMediaFormat.Decode):
mime_type = QMediaFormat(f).mimeType()
result.append(mime_type.name())
return result
# To be used on the @QmlElement decorator
# (QML_IMPORT_MINOR_VERSION is optional)
QML_IMPORT_NAME = "io.qt.textproperties"
QML_IMPORT_MAJOR_VERSION = 1
allimg = []
# index = 0
Result_in_queue_maxsize = 100
result_que = Queue(maxsize = Result_in_queue_maxsize)
# https://doc.qt.io/qt-6/qtquick-qmlmodule.html
# https://doc.qt.io/qtforpython-6/PySide6/QtQuick/index.html
class ThreadQ(QThread):
updateFrame = Signal(QPixmap)
def __init__(self, parent=None):
QThread.__init__(self, parent)
self.trained_file = None
self.status = True
self.cap = True
self.model = "yolov8n.pt"
self.input = "camera"
self.video = ""
self.checked = False
self.videowriter = True
self.image_stop = False
self.m = None
w, h = 300, 300
image = np.zeros((w, h, 3))
image = QImage(image.data, w, h, 3 * w, QImage.Format_RGB888)
self.zeros = QPixmap.fromImage(image)
def set_input(self, fname):
self.input = fname
def set_model(self):
if self.m!=None:
del self.m
import gc
gc.collect()
self.m = YOLO(self.model)
'''
#########################
method 1
#########################
'''
def run(self):
self.set_model()
if self.input == 'camera':
self.cap = cv2.VideoCapture(0)
elif self.input=='video':
self.cap = cv2.VideoCapture(self.video)
if self.checked:
fps = 24
sizes = (640, 500)
cvfont = cv2.FONT_HERSHEY_SIMPLEX
if not isinstance(self.cap, bool):
fps = self.cap.get(cv2.CAP_PROP_FPS)
sizes = (int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
if fps <= 0:
fps = 24
# fps = 10
movies_location = QStandardPaths.writableLocation(QStandardPaths.MoviesLocation)
if '/' in movies_location:
movies_location = movies_location.replace("/", os.sep)
if '\\' in movies_location:
movies_location = movies_location.replace("\\", os.sep)
date = datetime.now().isoformat().__str__().replace(".", "_").replace(":", "_")
outpath = movies_location + os.sep + date + ".avi"
self.videowriter = cv2.VideoWriter(outpath, cv2.VideoWriter_fourcc(*'XVID'), fps, sizes)
cnt = 0
if self.input in ["camera", "video"]:
ret = True
nu = 0
while ret:
if self.image_stop:
self.updateFrame.emit(self.zeros)
break
ret, frame = self.cap.read()
if not ret:
continue
result = self.m.predict(frame, batch = 1, stream=False)
frame = result[0].plot()
# cv2.putText(frame, str(fps)+"_"+str(sizes), (100, 100), cvfont, 0.5, [255, 0, 0], 1)
# cv2.putText(frame, str(nu), (360, 360), cvfont, 2, [255, 0, 0], 1)
nu += 1
if self.checked:
self.videowriter.write(frame)
out = r'C:\Users\10696\Desktop\CV\ZouJiu1\MagicTime'
cv2.imwrite(os.path.join(out, str(cnt)+".jpg"), frame)
cnt += 1
color_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# # Creating and scaling QImage
h, w, ch = color_frame.shape
img = QImage(color_frame.data, w, h, ch * w, QImage.Format_RGB888)
# Emit signal
self.updateFrame.emit(QPixmap.fromImage(img))
# sys.exit(-1)
else:
# outpath = r'C:\Users\10696\Desktop\CV\ZouJiu1\Pytorch_YOLOV3\log\output'
# nu = 0
for i in os.listdir(self.video):
if '.jpg' in i or '.jpeg' in i or '.png' in i or '.bmp' in i:
allimg.append(i)
for i in allimg:
if self.image_stop:
self.updateFrame.emit(self.zeros)
break
frame = cv2.imread(os.path.join(self.video, i))
result = self.m.predict(frame, batch = 1, stream=False)
frame = result[0].plot()
# cv2.imwrite(os.path.join(outpath, str(nu)+".jpg"), frame)
# nu += 1
if self.checked:
img = cv2.resize(frame, sizes)
self.videowriter.write(img)
color_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# Creating and scaling QImage
h, w, ch = color_frame.shape
img = QImage(color_frame.data, w, h, ch * w, QImage.Format_RGB888)
# Emit signal
self.updateFrame.emit(QPixmap.fromImage(img))
# allresult.append(result)
if self.checked:
self.videowriter.release()
self.videowriter = True
# updateModel = Signal(str)
# @Slot()
# def open_model(path:QUrl):
# global updateModel
# model = path.toString()[2**3:]
# updateModel.emit(model)
@QmlElement # 装饰器,表示信号传递给这些槽,下面的函数都是槽,信号都在qml文件中
# class Camera_video(QObject):
class Camera_video(QQuickImageProvider):
imageChange = Signal(bool)
def __init__(self):
# super().__init__(QQuickImageProvider)
super().__init__(QQmlImageProviderBase.ImageType.Pixmap)
# Thread in charge of updating the image
self.th = ThreadQ(self)
self._model = "yolov8n.pt"
# updateModel.connect(self.get_model)
self.th.updateFrame.connect(self.updateImage)
# self.th.finished.connect(self.setlast)
# self.nowstatus = self.saveState()
# inpath = r"C:\Users\10696\Desktop\CV\ZouJiu1\Pytorch_YOLOV3\log\val"
# self.imagelist = [os.path.join(inpath, i) for i in os.listdir(inpath)]
self.pixmap = self.th.zeros
def requestPixmap(self, id="image_elementll", size=QSize(0, 0),
requestedSize=QSize(0, 0)):
# w, h = 640, 600
# # image = np.zeros((w, h, 3)) * 255
# image = cv2.imread(np.random.choice(self.imagelist, 1)[0])
# image = cv2.resize(image, (w, h))
# h, w, ch = image.shape
# image = QImage(image.data, w, h, ch * w, QImage.Format_RGB888)
# pixmap = QPixmap.fromImage(image)
return self.pixmap
@Slot(QPixmap)
def updateImage(self, image):
self.pixmap = image
self.imageChange.emit(True)
@Slot(str, result=None)
def get_model(self, model):
self.th.model = model.replace("file:///", "")
@Slot(str, result=None)
def get_video(self, video):
self.th.video = video.replace("file:///", "")
@Slot(str, result=None)
def get_type(self, type):
self.th.input = type
@Slot(bool)
def get_checked(self, ischecked):
self.th.checked = ischecked
@Slot()
def start(self):
self.th.start()
# self.showFullScreen()
@Slot()
def kill_thread(self):
print("Finishing...")
if not isinstance(self.th.cap, bool):
self.th.cap.release()
if self.th.checked:
if not isinstance(self.th.videowriter, bool):
self.th.videowriter.release()
self.th.status = False
cv2.destroyAllWindows()
self.status = False
# Give time for the thread to finish
time.sleep(1)
self.th.terminate()
self.th.cap = True
self.th.videowriter = True
self.th.image_stop = False
@Slot()
def stop(self):
self.th.image_stop = True
self.kill_thread()
if __name__ == '__main__':
# https://doc.qt.io/qt-6/qmlapplications.html
app = QGuiApplication(sys.argv)
QQuickStyle.setStyle("Material")
# pyside6-rcc style.qrc -o style_rc.py
# Get the path of the current directory, and then add the name
# of the QML file, to load it.
qml_file = Path(__file__).parent / 'view.qml'
my_image_provider = Camera_video()
engine = QQmlApplicationEngine() # 分析qml文件
# https://stackoverflow.com/questions/72729052/how-to-show-opencv-camera-feed-in-a-qml-application#
# https://stackoverflow.com/questions/73684543/how-to-display-opencv-camera-feed-in-a-qml-pyside6-application?r=SearchResults#
engine.rootContext().setContextProperty("my_image_provider", my_image_provider)
engine.addImageProvider("My_image_provider", my_image_provider)
engine.load(qml_file)
if not engine.rootObjects():
sys.exit(-1)
# root = engine.rootObjects()[0]
# root.model_fileOpened.connect(open_model)
# view = QQuickView()
# view.setResizeMode(QQuickView.SizeRootObjectToView) # 自适应窗口大小,拽动也能保持不变
# view.setSource(QUrl.fromLocalFile(qml_file.resolve())) # 视图配置qml文件的加载路径
# view.show()
sys.exit(app.exec())
view.qml
// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Window
import QtQuick.Controls.Material
import QtQuick.Dialogs
import io.qt.textproperties
// 窗口
ApplicationWindow {
id: root
width: 790
height: 600
minimumHeight: 460
minimumWidth: 790
visible: true
// color: Config.mainColor
title: qsTr("Yolov* camera video")
Material.theme: Material.Dark
Material.accent: Material.Red
// Camera_video { // 和main.py中的槽类对应,将下面的信号signals connect到槽slot所在的类
// id: my_image_provider
// }
// Used as an example of a backend - this would usually be
// e.g. a C++ type exposed to QML.
QtObject {
id: backend
property int modifier
}
signal model_fileOpened(path: url)
//https://stackoverflow.com/questions/72729052/how-to-show-opencv-camera-feed-in-a-qml-application#
//https://stackoverflow.com/questions/73684543/how-to-display-opencv-camera-feed-in-a-qml-pyside6-application?r=SearchResults#
Connections { //https://doc.qt.io/qt-6/qtqml-syntax-signals.html
target: my_image_provider
function onImageChange(image) {
// console.log("emit")
image_element.reLoadImage()
}
}
FileDialog {
id: fileDialog_model
currentFolder: StandardPaths.standardLocations(StandardPaths.MoviesLocation)[0]
nameFilters: ["*"]
// selectedNameFilter.index: root.selectedNameFilter
title: qsTr("Please choose a model")
selectedFile: fileDialog_model.selectedFile
onAccepted: {
input_or_choose_mode_path.text = fileDialog_model.selectedFile.toString()
my_image_provider.get_model(fileDialog_model.selectedFile.toString())
}
}
FileDialog {
id: fileDialog_video_image
currentFolder: StandardPaths.standardLocations(StandardPaths.MoviesLocation)[0]
nameFilters: ["*"]
// selectedNameFilter.index: root.selectedNameFilter
title: qsTr("Please choose a video")
selectedFile: fileDialog_video_image.selectedFile
onAccepted: {
choose_video_or_image.text = fileDialog_video_image.selectedFile.toString()
my_image_provider.get_video(fileDialog_video_image.selectedFile.toString())
}
}
FolderDialog {
id: folderDialog
currentFolder: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
selectedFolder: viewer.folder
title: qsTr("Please choose a images directory")
onAccepted: {
choose_video_or_image.text = folderDialog.selectedFolder.toString()
my_image_provider.get_video(folderDialog.selectedFolder.toString())
}
}
GridLayout { // 布局2行1列
id: grid // 名称
anchors.fill: parent
anchors.margins: 2
columns: 1
rows: 3
rowSpacing: 1
columnSpacing: 1
RowLayout { //第一行
id: row1
spacing: 1
// Layout.columnSpan: 1
// Layout.preferredWidth: 400
// Layout.preferredHeight: 400
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Image { // 放置一个图片 https://doc.qt.io/qt-6/qml-qtquick-image.html
id: image_element
fillMode: Image.PreserveAspectFit // 填充模式
anchors.centerIn: root // 在矩形内部居中放置
source: "image://My_image_provider/img" // 图片路径
opacity: 1.0 // 不透明度
cache: false
// onImageChange {
// source: "image://Camera_video/"
// }
property int counter: 0
function reLoadImage() {
// counter!=counter
// source = "./logo.png"
source = "image://My_image_provider/img?id=" + counter
counter++
counter%=999999999
}
}
}
RowLayout { // 第2列
id: rightcolumn
spacing: 2
Layout.columnSpan: 1
// Layout.preferredWidth: 400
// Layout.preferredHeight: 100
Layout.fillWidth: true
// Layout.alignment: Qt.AlignBottom
Layout.alignment: Qt.AlignLeft
//Center
ComboBox {
id: combox
textRole: "text"
valueRole: "value"
// When an item is selected, update the backend.
// onActivated:
// Set the initial currentIndex to the value stored in the backend.
// Component.onCompleted: currentIndex = indexOfValue(backend.modifier)
model: [
{ value: Qt.NoModifier, text: qsTr("camera") },
{ value: Qt.ShiftModifier, text: qsTr("video") },
{ value: Qt.ControlModifier, text: qsTr("image directory") }
]
background: Rectangle {
implicitWidth: 130
implicitHeight: 40
color: "#353637"
border.color: "#21be2b"
}
onActivated: {
choose_video_or_image.enabled = combox.currentText=="camera"? false:true
video_or_image.enabled = combox.currentText=="camera"? false:true
}
}
TextField {
id: input_or_choose_mode_path
// placeholderText: qsTr("yolov8n.pt")
// placeholderTextColor: "#ffffff"
color: "#ffffff"
text: "yolov8n.pt"
background: Rectangle {
implicitWidth: 160
implicitHeight: 40
color: "#353637"
border.color: "#21be2b"
}
}
Button { // 按钮
id: model
text: "model"
highlighted: true
Material.accent: Material.Red
onClicked: { // 点击以后调用槽函数slot
// bridge.open()
fileDialog_model.open()
}
}
TextField {
id: choose_video_or_image
placeholderText: qsTr("video or images directory")
placeholderTextColor: "#ffffff"
color: "#ffffff"
text: ""
enabled: false
background: Rectangle {
implicitWidth: 160
implicitHeight: 40
color: "#353637"
border.color: "#21be2b"
}
}
Button {
id: video_or_image
text: "video or image"
highlighted: true
enabled:false
Material.accent: Material.Green
onClicked: { // 点击以后调用槽函数slot
combox.currentText=="video"? fileDialog_video_image.open():folderDialog.open()
}
}
}
RowLayout {
id: rightcolumn2
spacing: 2
// Layout.columnSpan: 1
// Layout.preferredWidth: 400
// Layout.preferredHeight: 400
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
Button { // 按钮
id: media_start
text: "media start"
highlighted: true
Material.accent: Material.Red
onClicked: { // 点击以后调用槽函数slot
my_image_provider.get_type(combox.currentText)
my_image_provider.get_model(input_or_choose_mode_path.text)
my_image_provider.get_video(choose_video_or_image.text)
combox.enabled = false
stop.enabled = true
media_start.enabled = false
model.enabled = false
input_or_choose_mode_path.enabled = false
choose_video_or_image.enabled = false
video_or_image.enabled = false
control.enabled = false
my_image_provider.start()
}
}
Button {
id: stop
text: "stop"
highlighted: true
Material.accent: Material.Green
onClicked: { // 点击以后调用槽函数slot
combox.enabled = true
media_start.enabled = true
model.enabled = true
input_or_choose_mode_path.enabled = true
control.enabled = true
choose_video_or_image.enabled = combox.currentText=="camera"? false:true
video_or_image.enabled = combox.currentText=="camera"? false:true
my_image_provider.stop()
stop.enabled = false
}
}
CheckBox {
id: control
text: qsTr("save video")
checked: false
background: Rectangle {
implicitWidth: 60
implicitHeight: 30
visible: control.down || control.highlighted
color: control.down ? "#bdbebf" : "#00ee00"
}
onClicked: {
my_image_provider.get_checked(control.checked)
}
}
}
}
}