Progressbar mit ProcessPoolExecutor

Fragen zu Tkinter.
Antworten
LRD36
User
Beiträge: 5
Registriert: Samstag 30. Oktober 2021, 11:32

Hallo,

ich habe mir bereits eine kleine Benutzeroberfläche mit tkinter zusammengebastelt, habe nun aber ein Problem damit den Fortschritt des darauf folgenden Prozesses darzustellen.
Hier eine reduzierte Version, welche die wichtigsten Strukturen meines Codes abbildet:

Code: Alles auswählen

import concurrent.futures, time
from PIL import Image

class Process():
    total_lines = 0
    
    def __init__(self, images):
        self.images = images
        for i in self.images:
            w, h = Image.open(i).size
            self.total_lines+=h

        self.t1 = time.perf_counter()
        with concurrent.futures.ProcessPoolExecutor() as executor:
            r = executor.map(self.start, self.images)
        self.t2 = time.perf_counter()

        print(f'Finished in {self.t2-self.t1} seconds')

    def start(self, img):
        w, h = Image.open(img).size

        for y1 in range(0, h):
            # Processing things here
            time.sleep(0.001)
                
        return f'Image {img} processed.'

if __name__ == "__main__":
    image_new = Image.new('RGB', (1000,1000), (255, 255, 255))

    for name in ['a', 'b', 'c']:
        image_new.save(f'{name}.png')

    images = ['./a.png', './b.png', './c.png']
    a = Process(images)
Also, die Klasse wird mit einer Bildliste gefüttert, die Höhendimension aller Bilder wird zusammenaddiert und in "total_lines" gespeichert.
Dann startet der Hauptprozess, in dem jedes Bild einzeln geöffnet, durch jede Pixelreihe iteriert wird und weitere Bearbeitungen stattfinden werden.

Mein Wunsch wäre es nun ein neues kleines Fenster zu haben in dem sich lediglich ein kleiner Ladebalken befindet, welcher an dieser Stelle anhand des for-loops und der Variable "total_lines" aktualisiert wird.

Code: Alles auswählen

        for y1 in range(0, h):
            # Processing things here
Kann mir hier vielleicht jemand weiterhelfen? Ich komme da irgendwie nicht weiter...
Vielen Dank im Voraus!
Benutzeravatar
Dennis89
User
Beiträge: 1155
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,
LRD36 hat geschrieben: Samstag 30. Oktober 2021, 13:15 Mein Wunsch wäre es nun ein neues kleines Fenster zu haben in dem sich lediglich ein kleiner Ladebalken befindet
Um ein weiteres Fenster zu öffnen kannst du 'toplevel' benutzen.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
LRD36
User
Beiträge: 5
Registriert: Samstag 30. Oktober 2021, 11:32

Danke für die Antwort! Mein Problem ist es nicht direkt ein neues Fenster zu erstellen und dieses mit einem Ladebalken zu versehen, sondern eher die Integration in den bestehenden Code. Wo macht es wie Sinn das Fenster zu erstellen und wie kann ich die Progressbar dann an der gewünschten Stelle aktualisieren? Ich habe durchaus ein Fenster mit Ladebalken hinbekommen, nur nicht dass sich der Ladebalken wie gewünscht aktualisiert.
Sirius3
User
Beiträge: 17749
Registriert: Sonntag 21. Oktober 2012, 17:20

total_lines sollte nicht auf Klassenebene definiert werden, sondern in __init__.
Das übliche Vorgehen ist, dass man einen Hintergrundthread hat, der die Arbeit macht und im Vordergrund läuft die GUI, die regelmäßig per after den Stand abfragt und darstellt.
LRD36
User
Beiträge: 5
Registriert: Samstag 30. Oktober 2021, 11:32

Ich bin noch nicht so erfahren mit tkinter und besonders nicht mit concurrent.futures, aber das größte Problem was ich in meinem Code sehe ist nicht die Erstellung eines Fensters, sondern der fehlende shared memory um überhaupt einen Prozessübergreifenden Status ausgeben zu können, oder sehe ich da etwas falsch...

Ich habe den oben stehenden Code noch etwas abgeändert dass mit der Fortschritt ausgegeben wird - funktioniert aber leider nur mit dem ThreadPoolExecutor richtig.

Code: Alles auswählen

import concurrent.futures, time
from PIL import Image

class Process():
    def __init__(self, images):
        self.line_count = 0
        self.total_lines = 0
        self.images = images
        for i in self.images:
            w, h = Image.open(i).size
            self.total_lines+=h

        self.t1 = time.perf_counter()
        with concurrent.futures.ThreadPoolExecutor() as executor:
            executor.map(self.start, self.images)
        self.t2 = time.perf_counter()

        print(f'Finished in {self.t2-self.t1} seconds')

    def start(self, img):
        w, h = Image.open(img).size

        for y1 in range(0, h):
            time.sleep(0.005)
            self.line_count+=1
            progress = int((self.line_count/self.total_lines)*100)
            print(progress)
            # Processing things here

if __name__ == "__main__":
    image_new = Image.new('RGB', (1000,1000), (255, 255, 255))

    for name in ['a', 'b', 'c', 'd']:
        image_new.save(f'{name}.png')

    images = ['./a.png', './b.png', './c.png', './d.png']
    a = Process(images)
__deets__
User
Beiträge: 14539
Registriert: Mittwoch 14. Oktober 2015, 14:29

Shared memory benutzt man in Python eher selten. Wenn du wirklich mehrere Prozesse benutzt, um echte Parallelität zu haben, musst du eben die einzelnen Fortschritte via eine Queue zb an den Hauptprozess rück melden.
LRD36
User
Beiträge: 5
Registriert: Samstag 30. Oktober 2021, 11:32

Ja es ist leider notwendig für mich einzelne Prozesse zu benutzen.
Kannst du mir da zufällig ein Beispiel zu zeigen, damit ich mir das besser vorstellen kann?
__deets__
User
Beiträge: 14539
Registriert: Mittwoch 14. Oktober 2015, 14:29

In der multiprocessing-Dokumentation sollte etwas zu finden sein.
Benutzeravatar
__blackjack__
User
Beiträge: 13102
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@LRD36: Anmerkungen zum Quelltext: `images` enthält gar keine Bilder sondern Dateinamen von Bildern. Das ist etwas verwirrend in einem Programm in dem es auch `Image`-Objekte gibt. Die Liste mit den Namen würde man am besten auch gleich zusammen mit den Bilddateien selbst erstellen, statt dort Informationen noch mal manuell zu wiederholen.

`Process` ist eine ganz komische Klasse. Da scheint die `__init__()` die komplette Arbeit zu machen und das Ergebnis wird zwar formal an einen sinnfreien/nichtssagenden Namen (`a`) gebunden, aber damit wird dann nichts gemacht. Eine `__init__()` sollte ein benutzbares Objekt erstellt und dann zum Aufrufer zurückkehren, damit der etwas mit dem Objekt machen kann. Wenn man das nicht hat, dann deutet das darauf hin, das die `__init__()` eigentlich gar nicht in eine Klasse gehört, sondern eine Funktion sein sollte. Oder ein Teil davon eine Methode auf dem Objekt, die dann später, nach dem Erstellen aufgerufen wird.

`i` und dann noch in einer Schleife ist ein schlechter Name für alles das keine ganze Zahl ist. Das sollte hier wohl `filename` oder `image_filename` heissen.

`images` wird unnötigerweise an das Objekt gebunden.

`line_count` ist eher etwas lokales von `start()`. Wenn man das in der `__init__()` setzt, dann muss man darauf vertrauen das der Leser, und das ist man als Autor letztlich ja auch selber, immer im Hinterkopf hat, dass das zwar hier nur einmal initiatialisiert wird, aber in der `start()` jeder Prozess seine eigene Kopie davon verändern wird.

Wenn man `line_count` in die `start()` verschiebt fällt dann auch gleich viel leichter auf, dass `line_count` immer den gleichen Werteverlauf hat, wie dass unbenutzte (und schlecht benannte) `y1`. Also kann man einfach `y1` in `line_count` umbenennen und kann sich das manuelle hochzählen sparen.

In der `__init__()` sind `t1` und `t2` eigentlich lokal und gehören nicht an das Objekt gebunden.

Zwischenstand: Das `Process` eine Klasse ist, hängt mittlerweile nur noch an *einem* Attribut: `self.total_lines`. Die `start()`-Methode greift da nur lesend drauf zu, das heisst man könnte den Wert auch einfach als Argument übergeben, und schon verschwindet die `Process`-Klasse, weil es keinen Zustand mehr gibt, den sie kapseln würde.

Die `map()`-Methode ist hier ja irgendwie falsch, weil Dich das Ergebnis überhaupt gar nicht interessiert.

Zwischenstand:

Code: Alles auswählen

#!/usr/bin/env python3
import concurrent.futures
import time

from PIL import Image


def load_image_height(filename):
    _width, height = Image.open(filename).size
    return height


def process_image(filename, total_lines):
    for line_count in range(1, load_image_height(filename) + 1):
        time.sleep(0.005)
        progress = int(line_count / total_lines * 100)
        print(progress)
        #
        # Processing things here.
        #


def process_images(filenames):
    total_lines = sum(map(load_image_height, filenames))

    start_time = time.perf_counter()
    with concurrent.futures.ThreadPoolExecutor() as executor:
        for filename in filenames:
            executor.submit(process_image, [filename, total_lines])
    end_time = time.perf_counter()

    print(f"Finished in {end_time - start_time} seconds")


def main():
    image = Image.new("RGB", (1000, 1000), (255, 255, 255))
    filenames = []
    for name in ["a", "b", "c", "d"]:
        filename = f"{name}.png"
        image.save(filename)
        filenames.append(filename)

    process_images(filenames)


if __name__ == "__main__":
    main()
Hier gibt ja jede Ausführung von `process_image()` (ehemals `Process.start()`) den eigenen Fortschritt bezogen auf die Gesamtzahl der Pixelzeilen aus. Aber in einem eigenen Prozess. Das Problem was ich sehe, ist das die `futures`-API es nicht vorsieht, dass die einzelnen Aufgaben während der Abarbeitung mit dem Auftraggeber kommunizieren. Und ich würde das da auch nicht an der API vorbei rein hacken wollen. Wenn Du also sowieso sicher bist, dass Du Prozesse brauchst, würde ich hier `multiprocessing` verwenden, denn das hat `Queue`-Objekte um mit dem Auftraggeber zu kommunizieren.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
LRD36
User
Beiträge: 5
Registriert: Samstag 30. Oktober 2021, 11:32

Wow, vielen Dank für deine ausführliche Antwort! Meine Process-Klasse ist eigentlich deutlich umfangreicher - ich wollte den Beitrag hier möglichst übersichtlich halten und nur den Kern meines Problems abbilden, weshalb das ganze jetzt nicht unbedingt so sinnvoll erscheinen mag. Du hast aber mit vielen Sachen recht und mich auf jeden Fall sehr weiter gebracht - nochmal vielen Dank für deine Bemühungen!
Ich bin deinem Rat von concurrent.futures auf multiprocessing zu wechseln auf jeden Fall gefolgt und kann mittlerweile den Fortschritt über Queue ausgeben - jetzt muss ich das nur noch in ein neues Tkinter Fenster bekommen.
Antworten