Threading - Kivy GUI updaten

Hier werden alle anderen GUI-Toolkits sowie Spezial-Toolkits wie Spiele-Engines behandelt.
Antworten
PyPat2021
User
Beiträge: 3
Registriert: Donnerstag 4. November 2021, 19:29

Guten Abend, ich bin ziemlich neu in Sachen Python und versuche schon seit längerer Zeit mein Problem zu lösen. Ich habe eine Benutzeroberfläche in Kivy und möchte gerne ein Bild von meiner IP-Kamera empfangen und dieses in der Benutzeroberfläche anzeigen lassen. Das sollte 30 mal pro Sekunde passieren, um ein Video mit etwa 30 fps zu erzeugen. Mein Problem ist, dass die Benutzeroberfläche einfriert. Man kann jedoch feststellen, dass der Code im Hintergrund richtig arbeitet: Wenn ich manuell auf das Fenster klicke und die Größe anpasse, wird in diesem Moment das momentane Bild der Kamera in der Benutzeroberfläche aktualisiert. Ich habe probiert, das ganze durch Threading zu lösen, aber mache hier wohl etwas falsch:

Code: Alles auswählen

#Das ist die Hauptfunktion
def main(self, *args):
    global adjustment_stop_threading
    global adjustment_lock
    global adjustment_frame
    
    #Hier starte ich in einem Thread die andere Funktion, die das aktuelle Bild der IP-Kamera abgreift
    threading.Thread(target=self.getimage_livestream).start()
 
    #"my_frame" sollte zu Beginn leer sein
    my_frame = None
        
    while True:
        #grab lock
        with adjustment_lock:
            #fortfahren, wenn da ein Bild in "adjustment_frame" ist 
            if not adjustment_frame is None:
                #we copy here to dump the lock as fast as possible
                my_frame = np.copy(adjustment_frame)
            
        if my_frame is not None:
            try:
                #etwas Bildbearbeitung folgt, um das Bild als KIVY-Textur anzeigen lassen zu können
                buf1 = cv2.flip(my_frame, 0)
                buf = buf1.tobytes()
                image_texture = Texture.create(size=(my_frame.shape[1], my_frame.shape[0]), colorfmt='rgb')
                image_texture.blit_buffer(buf, colorfmt='rgb', bufferfmt='ubyte')
                #Hier wird die KIVY-Textur "liveimageadjustment.texture" durch das neue Bild "image_texture" ersetzt
                self.root.ids.liveimageadjustment.texture = image_texture
                #sleep, um der Benutzeroberfläche zu erlauben, sich zu aktualisieren...das funktioniert jedoch nicht!
                time.sleep(0.03)
            except Exception as e:
                print(e)
                #hier wird keine Exception ausgegeben, da der Code im großen und Ganzen im Hintergrund funktioniert, lediglich die GUI aktualisiert sich nicht
        if cv2.waitKey(1) == ord('q'):
            break
 
            
#Diese Funktion verbindet sich mit der IP-Kamera, empfängt und dreht das aktuelle Bild. Dieses wird dann in der globalen Variablen "adjustment_frame" gespeichert
def getimage_livestream(self, *args):
        global adjustment_stop_threading
        global adjustment_lock
        global adjustment_frame
 
        stream = urlopen('http://192.168.4.1:81/stream')
        bytes = b''
        while not adjustment_stop_threading:
            try:
                bytes += stream.read(1024) 
                a = bytes.find(b'\xff\xd8') 
                b = bytes.find(b'\xff\xd9')
                if a != -1 and b != -1: 
                    jpg = bytes[a:b+2] 
                    bytes = bytes[b+2:]
                    getliveimage = cv2.imdecode(np.frombuffer(jpg, dtype=np.uint8), cv2.IMREAD_COLOR)
                    livestreamrotated1 = cv2.rotate(getliveimage, cv2.ROTATE_90_CLOCKWISE) #here I am rotating the image
                    
                    with adjustment_lock:
                        adjustment_frame = livestreamrotated1
 
                    time.sleep(0.03)
 
            except Exception as e:
                #Auch hier wird keine Exception ausgegeben, alles funktioniert wie es soll
                print(e)
                print("failed at this point")
                
Ich bitte um Hilfe. Vielen Dank vorab!
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Man darf keine endlos schleifen in GUIs haben. Du musst den mainloop von Kivy anwerfen, und deine Frames per Timer oder einem Signalisierungs -Mechanismus holen.
PyPat2021
User
Beiträge: 3
Registriert: Donnerstag 4. November 2021, 19:29

__deets__ hat geschrieben: Donnerstag 4. November 2021, 21:33 Man darf keine endlos schleifen in GUIs haben. Du musst den mainloop von Kivy anwerfen, und deine Frames per Timer oder einem Signalisierungs -Mechanismus holen.
Kannst du mir hierbei in meinem Fall einen kleinen konkreten Anstoß geben, bitte?
Ich weiß langsam nicht mehr, mit welcher Funktion ich hier anpacken soll...

Vielen Dank vorab für jede weitere Hilfe.
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Leider nein. Die Zeit mich da selbst einzuarbeiten habe ich nicht. Ich kann dir nur raten, das wenn möglich mit den VideoWidgets von Kivy selbst umzusetzen, wenn du kannst. Dann hat wer das schon für dich erledigt.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@PyPat2021: Insgesamt ist das in der Regel keine so gute Idee GUI-Rahmenwerke zu mischen. Der Tastendruck für den Abbruch muss von Kivy kommen und nicht als Abfrage von `cv2.waitKey()`. Und dann sollte man auch ordentlich aufräumen. Also beispielsweise dir Verbindung zur Videoquelle schliessen.

Warum verwendest Du ``global``? Das macht man nicht. Schon gar nicht mit Threads. Und wenn da sowieso schon eine Klasse verwendet wird, gibt es da so gar keine Ausrede mehr für.

Was sollen die unbenutzen ``*args`` bei den beiden Methoden?

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 Kopieren der Frame-Daten ist unnötig. Eine neue Aufnahme verändert diese Daten ja nicht, sondern erstellt ein neues, unabhängiges Objekt.

Man muss nicht jedes Zwischenergebnis an einen (schlechten) Namen binden. Andererseits ist es manchmal hilfreich Werte an gute Namen zu binden, statt beispielweise magische Indexzugriffe zu machen.

`bytes` ist ein schlechter Name für etwas anderes als den eingebauten `bytes`-Typ, den man damit verdeckt. Ich würde da auch eher ein `bytearray` verwenden, das ist veränderbar.

So wie der Code jetzt ist, kann es unter Umständen passieren, dass `a` grösser als `b` ist. `find()` kann man den Startversatz als Argument mitgeben. Also `b` auf jeden Fall nach `a` suchen.

Der Mechanismus um in Kivy regelmässig etwas zu machen ist die `Clock`-Klasse.

Ungetestet:

Code: Alles auswählen

#!/usr/bin/env python3
import queue
from queue import Queue
from threading import Event, Thread
from urllib.request import urlopen

import cv2
import numpy as np
from kivy.clock import Clock
from kivy.graphics.texture import Texture


def read_livestream_images(stop_event, image_queue):
    with urlopen("http://192.168.4.1:81/stream") as stream:
        data = bytearray()
        while not stop_event.is_set():
            try:
                data.append(stream.read(1024))
                try:
                    start_index = data.index(b"\xff\xd8")
                except IndexError:
                    pass
                else:
                    try:
                        end_index = data.find(b"\xff\xd9", start_index) + 2
                    except IndexError:
                        pass
                    else:
                        jpg_data = data[start_index:end_index]
                        del data[:end_index]
                        image_queue.put(
                            cv2.rotate(
                                cv2.imdecode(
                                    np.frombuffer(jpg_data, dtype=np.uint8),
                                    cv2.IMREAD_COLOR,
                                ),
                                cv2.ROTATE_90_CLOCKWISE,
                            )
                        )

            except Exception as error:
                print(error)
                print("failed at this point")


class App:
    def __init__(self):
        self.stop_event = Event()
        self.livestream_queue = Queue(maxsize=10)
        self.clock_event = Clock.schedule_interval(
            self.update_livestream_image
        )
        Thread(
            target=read_livestream_images,
            args=[self.stop_event, self.livestream_queue],
            daemon=True,
        ).start()

    ...

    def update_livestream_image(self, _delta_time):
        image = None
        try:
            while True:
                image = self.livestream_queue.get_nowait()
        except queue.Empty:
            pass

        height, width = image.shape
        texture = Texture.create(size=(width, height))
        texture.blit_buffer(
            cv2.flip(image, 0).tobytes(), colorfmt="rgb", bufferfmt="ubyte"
        )
        self.root.ids.liveimageadjustment.texture = texture
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

@__blackjack__ das waitkey ist OpenCVs Mittel, seinen mainloop zu treiben. Das braucht man schon denke ich. Zumindest ausprobieren muss man das.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@__deets__: Wozu wird die denn hier gebraucht? Es werden doch nur verschiedene Konvertierungsfunktionen aufgerufen die auf Speicher operieren. Keine GUI und nicht einmal I/O.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ah, der benutzt gar kein capture? Dann ist meine Bemerkung natürlich irrelevant.
Antworten