Probleme mit meinem selbst erstellten Webcam-GUI

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Muecke1982
User
Beiträge: 7
Registriert: Freitag 30. Juni 2023, 15:35

Hallo miteinander,

nach sehr langem Hin und Her habe ich es endlich geschafft, ein GUI zu erstellen, in dem man sich die am PC angeschlossenen Webcams anzeigen lassen kann – jede in einem separaten Fenster. Die Fenster kann man einzeln schließen oder über das Menü alle auf einmal.

Ich stehe aktuell vor zwei zentralen Problemen, die ich nicht in den Griff bekomme:
  1. Die einzelnen Kamera-Fenster kann ich zwar vergrößern, aber nicht wieder verkleinern.
    Weiß jemand, warum ich die Fenster nicht kleiner skalieren kann?
  2. Das Programm ist insgesamt extrem langsam: Es dauert lange zu laden und auch, bis eine Kamera angezeigt wird.
Könnte mir jemand eventuell Tipps geben, wie ich meine Probleme in den Griff bekommen könnte? Besonders wichtig ist für mich, dass das Zoomen auch ins kleinere funktioniert. Dass das Ganze langsam ist, stört zwar, ist aber nicht kriegsentscheidend.

Vielen Dank schon mal im Voraus!

Gruß
Mücke

Code: Alles auswählen

from PyQt6.QtWidgets import (
    QApplication, QLabel, QMainWindow, QMdiArea, QMdiSubWindow
)
from PyQt6.QtGui import QAction, QIcon, QImage, QPixmap
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal
import cv2

# Thread zur Kameraerkennung
class CameraDetectThread(QThread):
    detected = pyqtSignal(list)

    def run(self):
        cams = []
        for i in range(4):
            cap = cv2.VideoCapture(i)
            if cap.isOpened():
                cams.append(i)
            cap.release()
        self.detected.emit(cams)

# Thread zum Laden der Kamera
class CameraLoadThread(QThread):
    frame_ready = pyqtSignal(QImage)

    def __init__(self, cam_index):
        super().__init__()
        self.cam_index = cam_index
        self.running = True

    def run(self):
        cap = cv2.VideoCapture(self.cam_index)
        cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
        cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)

        while self.running:
            ret, frame = cap.read()
            if ret:
                frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                h, w, ch = frame.shape
                bytes_per_line = ch * w
                img = QImage(frame.data, w, h, bytes_per_line, QImage.Format.Format_RGB888)
                self.frame_ready.emit(img)
            self.msleep(30)
        cap.release()

    def stop(self):
        self.running = False
        self.wait()

# Kamera-Widget
class CameraWidget(QLabel):
    def __init__(self, cam_index):
        super().__init__()
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.setStyleSheet("background-color: black;")
        self.pixmap_orig = None

        self.setText("Loading...")
        self.setStyleSheet("font-size: 24px; color: white; background-color: rgba(0,0,0,150);")

        self.thread = CameraLoadThread(cam_index)
        self.thread.frame_ready.connect(self.update_image)
        self.thread.start()

    def update_image(self, img):
        self.pixmap_orig = QPixmap.fromImage(img)
        self.update_scaled_pixmap()

    def update_scaled_pixmap(self):
        if self.pixmap_orig:
            scaled = self.pixmap_orig.scaled(
                self.width(), self.height(),
                Qt.AspectRatioMode.KeepAspectRatio,
                Qt.TransformationMode.SmoothTransformation
            )
            self.setPixmap(scaled)

    def resizeEvent(self, event):
        self.update_scaled_pixmap()
        super().resizeEvent(event)

    def closeEvent(self, event):
        self.thread.stop()
        event.accept()

# Hauptfenster
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Multi-Kamera Viewer")
        # Hier das Icon setzen
        self.setWindowIcon(QIcon("camera_icon_home.png"))

        self.mdi_area = QMdiArea()
        self.setCentralWidget(self.mdi_area)
        self.cameras_open = []

        # Loading beim Start
        self.loading_label = QLabel("Loading", alignment=Qt.AlignmentFlag.AlignCenter)
        self.loading_label.setStyleSheet("font-size: 36px; color: white; background-color: black;")
        self.loading_window = QMdiSubWindow()
        self.loading_window.setWidget(self.loading_label)
        self.loading_window.setWindowFlags(Qt.WindowType.FramelessWindowHint)
        self.mdi_area.addSubWindow(self.loading_window)
        self.resize_loading()
        self.loading_window.show()

        self.dot_count = 0
        self.loading_timer = QTimer()
        self.loading_timer.timeout.connect(self.update_loading)
        self.loading_timer.start(500)

        QTimer.singleShot(100, self.start_camera_detection)
        self.mdi_area.resizeEvent = self.on_mdi_resize

    def resize_loading(self):
        self.loading_window.setGeometry(0, 0, self.mdi_area.width(), self.mdi_area.height())

    def on_mdi_resize(self, event):
        self.resize_loading()
        event.accept()

    def update_loading(self):
        self.dot_count = (self.dot_count + 1) % 4
        self.loading_label.setText("Loading" + "." * self.dot_count)

    def start_camera_detection(self):
        self.thread = CameraDetectThread()
        self.thread.detected.connect(self.init_menu)
        self.thread.start()

    def init_menu(self, available_cams):
        self.loading_timer.stop()
        self.loading_window.close()

        menu_bar = self.menuBar()
        cam_menu = menu_bar.addMenu("Kameras")

        for idx in available_cams:
            action = QAction(f"Kamera {idx}", self)
            action.triggered.connect(lambda checked, i=idx: self.open_camera(i))
            cam_menu.addAction(action)

        close_all_action = QAction("Alle Kameras schließen", self)
        close_all_action.triggered.connect(self.close_all_cameras)
        cam_menu.addSeparator()
        cam_menu.addAction(close_all_action)

    def open_camera(self, cam_index):
        cam_window = QMdiSubWindow()
        cam_widget = CameraWidget(cam_index)
        cam_window.setWidget(cam_widget)
        cam_window.setWindowTitle(f"Kamera {cam_index}")
        cam_window.setWindowIcon(QIcon("camera_icon_home.png"))
        cam_window.resize(640, 480)
        self.mdi_area.addSubWindow(cam_window)
        cam_window.show()
        self.cameras_open.append(cam_window)

    def close_all_cameras(self):
        for win in self.cameras_open:
            win.close()
        self.cameras_open.clear()

if __name__ == "__main__":
    import sys
    app = QApplication(sys.argv)
    window = MainWindow()
    window.resize(1400, 900)
    window.show()
    sys.exit(app.exec())
Benutzeravatar
__blackjack__
User
Beiträge: 14135
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Muecke1982: Die (minimale) Fenstergrösse richtet sich nach dem Inhalt. Der muss Platz finden. Es kann also nicht kleiner werden als es Platz braucht das Bild im Label vollständig anzuzeigen. Schau Dir mal die Properties rund im `sizeHint` an. Da müsste Dein abgeleitetes Label wahrscheinlich signalisieren, dass es auch kleiner werden/sein darf.
“It is easier to change the specification to fit the program than vice versa.” — Alan J. Perlis
Muecke1982
User
Beiträge: 7
Registriert: Freitag 30. Juni 2023, 15:35

ich habe das nun etwas angepast:
Jetzt geht es. :-)

danke !


Das Programm ist echt langsam. :-( Naja, dafür funktioniert es. :-)

Code: Alles auswählen

from PyQt6.QtWidgets import (
    QApplication, QLabel, QMainWindow, QMdiArea, QMdiSubWindow, QSizePolicy
)
from PyQt6.QtGui import QAction, QIcon, QImage, QPixmap
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal
import cv2

# Thread zur Kameraerkennung
class CameraDetectThread(QThread):
    detected = pyqtSignal(list)

    def run(self):
        cams = []
        for i in range(4):
            cap = cv2.VideoCapture(i)
            if cap.isOpened():
                cams.append(i)
            cap.release()
        self.detected.emit(cams)

# Thread zum Laden der Kamera
class CameraLoadThread(QThread):
    frame_ready = pyqtSignal(QImage)

    def __init__(self, cam_index):
        super().__init__()
        self.cam_index = cam_index
        self.running = True

    def run(self):
        cap = cv2.VideoCapture(self.cam_index)
        cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
        cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)

        while self.running:
            ret, frame = cap.read()
            if ret:
                frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                h, w, ch = frame.shape
                bytes_per_line = ch * w
                img = QImage(frame.data, w, h, bytes_per_line, QImage.Format.Format_RGB888)
                self.frame_ready.emit(img)
            self.msleep(30)
        cap.release()

    def stop(self):
        self.running = False
        self.wait()

# Kamera-Widget
class CameraWidget(QLabel):
    def __init__(self, cam_index):
        super().__init__()
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.setStyleSheet("background-color: black;")
        self.pixmap_orig = None

        self.setText("Loading...")
        self.setStyleSheet("font-size: 24px; color: white; background-color: rgba(0,0,0,150);")

        # Fenster frei verkleinerbar machen
        self.setMinimumSize(100, 75)
        self.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored)

        self.thread = CameraLoadThread(cam_index)
        self.thread.frame_ready.connect(self.update_image)
        self.thread.start()

    def update_image(self, img):
        self.pixmap_orig = QPixmap.fromImage(img)
        self.update_scaled_pixmap()

    def update_scaled_pixmap(self):
        if self.pixmap_orig:
            scaled = self.pixmap_orig.scaled(
                self.width(), self.height(),
                Qt.AspectRatioMode.KeepAspectRatio,
                Qt.TransformationMode.SmoothTransformation
            )
            self.setPixmap(scaled)

    def resizeEvent(self, event):
        self.update_scaled_pixmap()
        super().resizeEvent(event)

    def closeEvent(self, event):
        # Nur den Thread stoppen, nicht die ganze App
        self.thread.stop()
        super().closeEvent(event)

# Hauptfenster
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Multi-Kamera Viewer")
        self.setWindowIcon(QIcon("camera_icon_home.png"))

        self.mdi_area = QMdiArea()
        self.setCentralWidget(self.mdi_area)
        self.cameras_open = []

        # Loading beim Start
        self.loading_label = QLabel("Loading", alignment=Qt.AlignmentFlag.AlignCenter)
        self.loading_label.setStyleSheet("font-size: 36px; color: white; background-color: black;")
        self.loading_window = QMdiSubWindow()
        self.loading_window.setWidget(self.loading_label)
        self.loading_window.setWindowFlags(Qt.WindowType.FramelessWindowHint)
        self.mdi_area.addSubWindow(self.loading_window)
        self.resize_loading()
        self.loading_window.show()

        self.dot_count = 0
        self.loading_timer = QTimer()
        self.loading_timer.timeout.connect(self.update_loading)
        self.loading_timer.start(500)

        QTimer.singleShot(100, self.start_camera_detection)
        self.mdi_area.resizeEvent = self.on_mdi_resize

    def resize_loading(self):
        self.loading_window.setGeometry(0, 0, self.mdi_area.width(), self.mdi_area.height())

    def on_mdi_resize(self, event):
        self.resize_loading()
        event.accept()

    def update_loading(self):
        self.dot_count = (self.dot_count + 1) % 4
        self.loading_label.setText("Loading" + "." * self.dot_count)

    def start_camera_detection(self):
        self.thread = CameraDetectThread()
        self.thread.detected.connect(self.init_menu)
        self.thread.start()

    def init_menu(self, available_cams):
        self.loading_timer.stop()
        self.loading_window.close()

        menu_bar = self.menuBar()
        cam_menu = menu_bar.addMenu("Kameras")

        for idx in available_cams:
            action = QAction(f"Kamera {idx}", self)
            action.triggered.connect(lambda checked, i=idx: self.open_camera(i))
            cam_menu.addAction(action)

        close_all_action = QAction("Alle Kameras schließen", self)
        close_all_action.triggered.connect(self.close_all_cameras)
        cam_menu.addSeparator()
        cam_menu.addAction(close_all_action)

    def open_camera(self, cam_index):
        cam_window = QMdiSubWindow()
        cam_widget = CameraWidget(cam_index)
        cam_window.setWidget(cam_widget)
        cam_window.setWindowTitle(f"Kamera {cam_index}")
        cam_window.setWindowIcon(QIcon("camera_icon_home.png"))
        cam_window.resize(640, 480)
        self.mdi_area.addSubWindow(cam_window)
        cam_window.show()
        self.cameras_open.append(cam_window)

    def close_all_cameras(self):
        for win in self.cameras_open:
            win.close()
        self.cameras_open.clear()

if __name__ == "__main__":
    import sys
    app = QApplication(sys.argv)
    window = MainWindow()
    window.resize(1400, 900)
    window.show()
    sys.exit(app.exec())
Benutzeravatar
__blackjack__
User
Beiträge: 14135
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Anmerkungen zum Code: Importe stehen am Anfang des Moduls, damit man leicht die Abhängigkeiten sehen kann.

Kommentare sollen dem Leser einen Mehrwert über den Code geben. Faustregel: Kommentare beschreiben nicht *was* der Code macht, denn das steht da bereits als Code, sondern warum er das macht. Sofern das nicht offensichtlich ist. Offensichtlich ist in aller Regel auch was in der Dokumentation von Python und den verwendeten Bibliotheken steht.

Das "Loading" mit den animierten Punkten würde ich aus dem Hauptfenster raus nehmen. Das sind mehrere Attribute die da eigentlich gar nichts mit zu tun haben, und die nur am Anfang mal sinnvoll benutzt werden.

Wenn man einzelne Kamera-Fenster schliesst, werden die nicht aus der Liste mit den offenen Kamerafenstern entfernt.

Das `running`-Attribut ist überflüssig, `QThread` hat so etwas schon selbst: `requestInterruption()`/`isInterruptionRequested()`.

Bei der Geschwindigkeit müsstest Du mal schauen wo die Zeit verbraucht wird. Ich vermute mal 132 mal pro Sekunde 1920×1080 Pixel grosse Bilder skalieren wird ein bisschen Last erzeugen.
“It is easier to change the specification to fit the program than vice versa.” — Alan J. Perlis
Antworten