Bilder mit PIL/Tkinter schnell skalieren

Fragen zu Tkinter.
Antworten
3komma141592
User
Beiträge: 4
Registriert: Sonntag 27. Mai 2018, 14:25

Moin,

ich möchte in einem Canvas eine kleine Animation erstellen. Ich habe dafür eine "Wiese" aus vielen Ovalen erzeugt, auf welcher ein Bild platziert wird. Jetzt will ich in die Szene hineinzoomen. Für die Ovale geht das ziemlich einfach mit canvas.scale(), um das Bild zu skalieren habe ich jeweils das Originalbild mit neu skaliert und mit dem vorherigen Bild ersetzt (siehe Code).

Problem: bei meinem (recht leistungsschwachen) Computer komme ich damit nur auf etwa 6 Frames pro Sekunde. Gibt es noch etwas, das ich optimieren könnte, um es etwa 4 mal so schnell zu bekommen?

Alternativ könnte ich mir auch vorstellen erst alle in der Größe angepassten Bilder als Liste zu speichern und dann immer nur das Bild zu ersetzen, und man könnte natürlich wie bei "professionellen" Animationen das ganze nicht live, sondern als Video ausgeben lassen. Diese Möglichkeiten wären für mich aber erst mal zweite Wahl.

Hier mein Code:

Code: Alles auswählen

#Hier den Dateipfad des Bildes angeben
path="/pfad/zum/ordner/vom/bild/"
from tkinter import *
from random import randint
from PIL import Image, ImageTk
from time import sleep,time

#Erzeugt einen Canvas im Fullscreen
root=Tk()
width=root.winfo_screenwidth()
height=root.winfo_screenheight()
root.geometry(str(width)+"x"+str(height)+"+0+0")
canvas=Canvas(root, width=width,height=height)
canvas.pack()

#Generiert eine "Wiese"
ovals=100
for i in range(ovals):
    x0 = randint(int(-width*0.15), width)
    y0 = randint(int(height*0.8),int(height*0.9))+height*0.1*(i/ovals)
    x1= x0+randint(int(width*0.05),int(width*0.3))
    y1 = y0+randint(height//10,height//5)
    g=i*(100//ovals)+155
    canvas.create_oval(x0,y0,x1,y1,outline="",fill="#%02x%02x%02x" %(50,g,50))

#zeigt ein Bild
image_raw = Image.open(path+"xxxxxxx.png")#hier Bild einfügen
image_size=(72,128)#hier Startgröße angeben
image=image_raw.resize(image_size,Image.NEAREST)
photo = ImageTk.PhotoImage(image)
image_id = canvas.create_image(width/2, height*0.7, image=photo, anchor=NW)
root.update()

fps=canvas.create_text(150,150,text=0)
sleep(2)
stime=time()
zoom_factor=1.01
frames=0

while True:
    #Skaliert die Ovale, verschiebt x0/y0 vom Bild
    canvas.scale("all",width/2,height*0.85,zoom_factor,zoom_factor)

    image_size=[i*zoom_factor for i in image_size]
    normed=[int(i) for i in image_size] #.resize benötigt Integer
    image=image_raw.resize(normed,Image.ANTIALIAS)
    photo = ImageTk.PhotoImage(image)
    canvas.itemconfig(image_id,image=photo)

    #FPS-Zähler
    frames+=1
    if frames==10:
        frames=0
        canvas.itemconfig(fps,text=int(1/(time()-stime)*10))
        stime=time()
    canvas.coords(fps,150,150)

    root.update()
Grüße, Pi :D
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@3komma141592: Ein paar Anmerkungen zum Code:

Vor Importen kommen nur Kommentare (insbesondere She-Bang und Kodierungskommentar) und der Docstring für das Modul.

Sternchen-Importe sind Böse™. Damit importiert man nicht nur alles was das Modul definiert, sondern auch was das importierte Modul selbst aus anderen Modulen importiert hat. Das wird ganz schnell unübersichtlich und dann kann man sich Module auch gleich sparen. Gerade beim `tkinter`-Modul holt man sich *hunderte* Namen ins Modul von denen nur ein ganz kleiner Bruchteil tatsächlich benötigt wird. Es besteht auch die Gefahr von Namenskollisionen. `tkinter` definiert beispielsweise auch ein `Image`.

Auf Modulebene sollte nur Code stehen, der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Konstanten werden per Konvention KOMPLETT_GROSS geschrieben.

Pfadteile setzt man nicht mit Zeichenkettenoperationen zusammen. Dafür gibt es das `pathlib`-Modul.

Zeichenketten und Werte mit ``+`` und `str()` zusammenstückeln ist eher BASIC als Python. In Python gibt es dafür Zeichenkettenformatierung mit der `format()`-Methode auf Zeichenketten und ab Python 3.6 auch f-Zeichenkettenliterale.

Namen sollten nicht abgekürzt werden solange es keine allgemein bekannten Abkürzungen sind. `fps` ist okay, aber `g` und `stime` nicht.

Mir ist nicht so ganz klar was an `image_raw` ”roh” sein soll‽

Selbst versuchen eine GUI-Hauptschleife zu schreiben ist keine gute Idee. Das skaliert nicht und kann auch zu subtilen Problemen führen. `sleep()` gehört nicht in GUI-Code.

Zur Zeitmessung ist `time.monotonic()` geeigneter als `time.time()`.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import time
import tkinter as tk
from pathlib import Path
from random import randint

from PIL import Image, ImageTk

# Hier den Dateipfad des Bildes angeben
PATH = Path("/pfad/zum/ordner/vom/bild")
ZOOM_FACTOR = 1.01


def do_zoom_step(
    canvas,
    width,
    height,
    image_size,
    image,
    image_id,
    fps_id,
    frame_count,
    start_time,
):
    canvas.scale(tk.ALL, width / 2, height * 0.85, ZOOM_FACTOR, ZOOM_FACTOR)

    image_size = [i * ZOOM_FACTOR for i in image_size]
    photo = ImageTk.PhotoImage(
        image.resize([int(i) for i in image_size], Image.ANTIALIAS)
    )
    canvas.itemconfig(image_id, image=photo)
    canvas.photo = photo

    # FPS-Zähler
    frame_count += 1
    if frame_count == 10:
        frame_count = 0
        canvas.itemconfig(
            fps_id, text=int(1 / (time.monotonic() - start_time) * 10)
        )
        start_time = time.monotonic()
    canvas.coords(fps_id, 150, 150)
    canvas.after(
        0,
        do_zoom_step,
        canvas,
        width,
        height,
        image_size,
        image,
        image_id,
        fps_id,
        frame_count,
        start_time,
    )


def start_zooming(canvas, width, height, image_size, image, image_id, fps_id):
    do_zoom_step(
        canvas,
        width,
        height,
        image_size,
        image,
        image_id,
        fps_id,
        0,
        time.monotonic(),
    )


def main():
    root = tk.Tk()
    width = root.winfo_screenwidth()
    height = root.winfo_screenheight()
    root.geometry(f"{width}x{height}+0+0")
    canvas = tk.Canvas(root, width=width, height=height)
    canvas.pack()

    # Generiert eine "Wiese".
    oval_count = 100
    for i in range(oval_count):
        x_0 = randint(int(-width * 0.15), width)
        y_0 = randint(int(height * 0.8), int(height * 0.9)) + height * 0.1 * (
            i / oval_count
        )
        x_1 = x_0 + randint(int(width * 0.05), int(width * 0.3))
        y_1 = y_0 + randint(height // 10, height // 5)
        green = i * (100 // oval_count) + 155
        canvas.create_oval(
            x_0,
            y_0,
            x_1,
            y_1,
            outline="",
            fill=f"#{50:02x}{green:02x}{50:02x}",
        )

    image = Image.open(PATH / "xxxxxxx.png")  # hier Bild einfügen
    image_size = (72, 128)  # hier Startgröße angeben
    photo = ImageTk.PhotoImage(image.resize(image_size, Image.ANTIALIAS))
    image_id = canvas.create_image(
        width / 2, height * 0.7, image=photo, anchor=tk.NW
    )
    fps_id = canvas.create_text(150, 150, text=0)
    canvas.after(
        2_000,
        start_zooming,
        canvas,
        width,
        height,
        image_size,
        image,
        image_id,
        fps_id,
    )
    root.mainloop()


if __name__ == "__main__":
    main()
Es müssen zu viele Werte als Argumente herum zu reichen, da sollte anfangen die sinnvoll zu Objekten zusammenzufassen. Generell kommt man im objektorientierte Programmierung bei GUIs nicht wirklich drum herum.

Die Erzeugung der Wiese könnte man auch aus der Hauptfunktion heraus ziehen. Wenn man die Funktion sinnvoll benennt, braucht man den Kommentar über dem Code-Block nicht mehr.

Zur Frage: Vorberechnen wäre eine Lösung. Ansonsten vielleicht ein anderes GUI-Rahmenwerk.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
__deets__
User
Beiträge: 14533
Registriert: Mittwoch 14. Oktober 2015, 14:29

Der Grund warum 3D (oder skalierte 2D) Animationen so locker und mit hohen Frameraten funktionieren liegt daran, dass dazu auf spezialisierte Hardware zugegriffen wird. ZB durch APIs wie OpenGL oder DirectX. Die gibt’s auch für Python - PIL ist aber NICHT dazu gedacht.
3komma141592
User
Beiträge: 4
Registriert: Sonntag 27. Mai 2018, 14:25

Vielen Dank für die vielen Hinweise! Habe mir meine Python-Kenntnisse hauptsächlich aus dem Internet zusammengebastelt, da bekommt man vor allem Formsachen nicht so mit. Dann schaue ich mich jetzt mal nach einem anderen Framework um.

Schönen Sonntag euch noch :)
Antworten