如何在 QML/Pyside6 应用程序中显示 Opencv 相机源?

问题描述 投票:0回答:1

我的问题类似于如何在 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

不知道这是否是正确的方法,但效果很好。

python qt opencv qml pyside6
1个回答
0
投票

这些代码对我帮助很大,如下所示。我使用 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()
        }
            
    }

我写的代码可以成功运行。检测、分割、姿势检测。人类等等。

  • 运行命令
  • pip 安装 pyside6
  • pip 安装 ultralytics
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)
                }
            }
        }
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.