23. Animované obrázky


prezentácia

video prezentácia


V priečinku obrázkov obrazky1.zip máme pripravené obrázky pre dnešnú prednášku, napríklad aj týchto 8 fáz animácií vtáčika v súbore 'vtak.png':

_images/23_1.png

Na 21. prednáške sme sa naučili takéto súbory rozstrihať do série obrázkov a potom sme to otestovali nejako takto:

import tkinter
from PIL import Image, ImageTk

def strihaj(meno_suboru, n):
    obr = Image.open(meno_suboru)
    zoz = []
    sir, vys = obr.width//n, obr.height
    for x in range(0, obr.width, sir):
        zoz.append(ImageTk.PhotoImage(obr.crop((x, 0, x+sir, vys))))
    return zoz

canvas = tkinter.Canvas()
canvas.pack()

zoz = strihaj('vtak.png', 8)
tk_id = canvas.create_image(200, 150)
faza = 0
while True:
    canvas.itemconfig(tk_id, image=zoz[faza])
    faza = (faza + 1) % len(zoz)
    canvas.update()
    canvas.after(100)

Použili sme tu funkciu strihaj(), ktorá je podobná funkcii z cvičení. V tejto verzii funkcia dostáva meno súboru, ktorý treba rozstrihať a vytvorí zoznam obrázkov ale už prekonvertované do formátu PhotoImage pre tkinter.


Animované objekty

Ak by sme chceli mať v ploche naraz tri animované obrázky, mohli by sme to zapísať takto:

import tkinter
from PIL import Image, ImageTk

def strihaj(meno_suboru, n):
    obr = Image.open(meno_suboru)
    sir, vys = obr.width//n, obr.height
    zoz = []
    for x in range(0, obr.width, sir):
        zoz.append(ImageTk.PhotoImage(obr.crop((x, 0, x+sir, vys))))
    return zoz

canvas = tkinter.Canvas()
canvas.pack()

zoz = strihaj('vtak.png', 8)
tk_id1 = canvas.create_image(200, 120)
tk_id2 = canvas.create_image(100, 80)
tk_id3 = canvas.create_image(300, 100)
faza = 0
while True:
    canvas.itemconfig(tk_id1, image=zoz[faza])
    canvas.itemconfig(tk_id2, image=zoz[faza])
    canvas.itemconfig(tk_id3, image=zoz[faza])
    faza = (faza + 1) % len(zoz)
    canvas.update()
    canvas.after(100)

Každý z týchto animovaných obrázkov používa ten istý zoznam obrázkov fáz animácie. Animovaný obrázok teraz zapuzdrime do triedy Anim:

import tkinter
from PIL import Image, ImageTk

class Anim:
    def __init__(self, x, y, zoz):
        self.id = canvas.create_image(x, y)
        self.zoz = zoz
        self.faza = 0

    def dalsia_faza(self):
        self.faza = (self.faza + 1) % len(self.zoz)
        canvas.itemconfig(self.id, image=self.zoz[self.faza])

def strihaj(meno_suboru, n):
    obr = Image.open(meno_suboru)
    sir, vys = obr.width//n, obr.height
    zoz = []
    for x in range(0, obr.width, sir):
        zoz.append(ImageTk.PhotoImage(obr.crop((x, 0, x+sir, vys))))
    return zoz

canvas = tkinter.Canvas()
canvas.pack()

zoz = strihaj('vtak.png', 8)
a1 = Anim(200, 120, zoz)
a2 = Anim(100, 80, zoz)
a3 = Anim(300, 100, zoz)
while True:
    a1.dalsia_faza()
    a2.dalsia_faza()
    a3.dalsia_faza()
    canvas.update()
    canvas.after(100)

Bude to fungovať aj vtedy, keď namiesto troch premenných a1, a2, a3, vyrobíme zoznam objektov, napríklad takto:

azoz = [Anim(200, 120, zoz),
        Anim(100, 80, zoz),
        Anim(300, 100, zoz),
        Anim(150, 200, zoz)]
while True:
    for a in azoz:
        a.dalsia_faza()
    canvas.update()
    canvas.after(100)

Zrejme v takomto zozname by teraz mohol byť ľubovoľný počet takýchto objektov.


Udalosti

Namiesto while-cyklu, v ktorom sa hýbu všetky animované objekty, to môžeme prepísať pomocou časovača:

def timer():
    for a in azoz:
        a.dalsia_faza()
    canvas.after(100, timer)

timer()

Teraz by fungovalo aj pridávanie nových objektov počas animovania tých existujúcich. Môžeme v konzole zapísať aj počas behu animácie, napríklad:

>>> azoz.append(Anim(280, 200, zoz))

Vďaka tomuto mechanizmu môžeme potupne pridávať nové a nové objekty a tie sa okamžite začnú animovať.

Teraz zoznam vyprázdnime a nové animované objekty budeme pridávať pri každom kliknutí myšou:

def timer():
    for a in azoz:
        a.dalsia_faza()
    canvas.after(100, timer)

def klik(event):
    azoz.append(Anim(event.x, event.y, zoz))

canvas = tkinter.Canvas()
canvas.pack()

zoz = strihaj('vtak.png', 8)
azoz = []
timer()
canvas.bind('<ButtonPress>', klik)

Takto môžeme vytvoriť ľubovoľný počet animovaných objektov.


Trieda animovaná Plocha

Grafickú aplikáciu spolu s canvasom a udalosťami zapuzdrime do triedy Plocha:

import tkinter
from PIL import Image, ImageTk

class Plocha:
    def __init__(self):
        self.canvas = Anim.canvas = tkinter.Canvas()
        self.canvas.pack()
        self.zoz = strihaj('vtak.png', 8)
        self.azoz = []
        self.timer()
        self.canvas.bind('<ButtonPress>', self.klik)

    def timer(self):
        for a in self.azoz:
            a.dalsia_faza()
        self.canvas.after(100, self.timer)

    def klik(self, event):
        self.azoz.append(Anim(event.x, event.y, self.zoz))

class Anim:
    canvas = None
    def __init__(self, x, y, zoz):
        self.id = self.canvas.create_image(x, y)
        self.zoz = zoz
        self.faza = 0

    def dalsia_faza(self):
        self.faza = (self.faza + 1) % len(self.zoz)
        self.canvas.itemconfig(self.id, image=self.zoz[self.faza])

def strihaj(meno_suboru, n):
    obr = Image.open(meno_suboru)
    sir, vys = obr.width//n, obr.height
    zoz = []
    for x in range(0, obr.width, sir):
        zoz.append(ImageTk.PhotoImage(obr.crop((x, 0, x+sir, vys))))
    return zoz

Plocha()

Keďže canvas už nebude globálna premenná, ale je to teraz atribút triedy Plocha, priradíme ho aj do triedneho atribútu canvas triedy Anim. Teraz sa takto k nemu dostanú aj inštancie triedy Anim.


Obrázok v pozadí grafickej plochy

Našou najbližšou úlohou bude vložiť daný obrázok ako pozadie grafickej plochy. Zatiaľ sme to robili nejako takto:

  • zistili sme si veľkosť obrázka (napríklad obrázok v súbore 'les.png' je veľký 1280``x``817 pixelov)

  • pri vytváraní canvas sme toto nastavili ako veľkosť a ako prvé sme potom do plochy vložili pomocou create_image tento obrázok

sir, vys = 1280, 817
canvas = tkinter.Canvas(width=sir, height=vys)
canvas.pack()
pozadie = tkinter.PhotoImage(file='les.png')
canvas.create_image(sir/2, vys/2, image=pozadie)

Keďže create_image(x, y, image=obrázok) umiesňuje obrázok tak, že (x, y) je stred obrázka, museli sme najprv prepočítať tieto súradnice na stred grafickej plochy. Pre nás by bolo pohodlnejšie, keby sme mohli určiť, ako sa takýto obrázok umiestni, t.j. ako sa ukotví (podobne ako sme ukotvovali texty v create_text) v grafickej ploche. Na to slúži pomenovaný parameter anchor, v ktorom zadáme ukotvenie 'nw' (teda na sevorozápadný roh obrázka) a súradnice (0, 0). Zapíšeme to takto:

canvas.create_image(0, 0, image=pozadie, anchor='nw')

Radi by sme ale namiesto priradenia sir, vys = 1280, 817 pre konkrétny obrázok pozadia, použili:

pozadie = tkinter.PhotoImage(file='les.png')
sir, vys = pozadie.width(), pozadie.height()
canvas = tkinter.Canvas(width=sir, height=vys)
canvas.pack()
canvas.create_image(0, 0, image=pozadie, anchor='nw')

Teda, že najprv prečítame obrázok, zistíme si jeho rozmery a až potom vytvárame grafickú plochu.

Toto nám spôsobí takúto chybovú správu:

RuntimeError: Too early to create image``.

Python nám tu vysvetľuje, že volanie tkinter.PhotoImage(...) prišlo skôr, ako sme vytvorili samotnú grafickú aplikáciu (zrejme pomocou tkinter.Canvas(...)).

Našťastie takto zacyklený problém vieme vyriešiť:

  • volanie:

    canvas = tkinter.Canvas()
    

    ktoré vytvára grafickú aplikáciu je len skratkou týchto dvoch príkazov:

    win = tkinter.Tk()
    canvas = tkinter.Canvas(win)
    
  • kde inštancia win reprezentuje samotnú grafickú aplikáciu (okno, window) na začiatku ešte bez canvasu (často sa zvykne tejto inštancii dávať meno root)

  • až druhý príkaz tkinter.Canvas(win) vytvorí canvas a umiestni ho do grafického okna win (parameter win zapisovať nemusíme, tkinter si ho domyslí)

A to je ten moment, keď môžeme ešte pred vytvorením canvasu prečítať nejaký obrázok zo súboru:

win = tkinter.Tk()
pozadie = tkinter.PhotoImage(file='les.png')
sir, vys = pozadie.width(), pozadie.height()
canvas = tkinter.Canvas(width=sir, height=vys)
canvas.pack()
canvas.create_image(0, 0, image=pozadie, anchor='nw')

Zapamätajme si, že tkinter nedovolí čítať obrázky (pomocou tkinter.PhotoImage) skôr, ako sa vytvorí okno grafickej aplikácie tkinter.Tk.

Naša aplikácia teraz vyzerá takto:

import tkinter
from PIL import Image, ImageTk

class Plocha:
    def __init__(self, meno_pozadia):
        self.pozadie = tkinter.PhotoImage(file=meno_pozadia)
        sir, vys = self.pozadie.width(), self.pozadie.height()
        self.canvas = Anim.canvas = tkinter.Canvas(width=sir, height=vys)
        self.canvas.pack()
        self.canvas.create_image(0, 0, image=self.pozadie, anchor='nw')
        self.zoz = strihaj('vtak.png', 8)
        self.azoz = []
        self.timer()
        self.canvas.bind('<ButtonPress>', self.klik)

    ...

...

win = tkinter.Tk()
Plocha('les.png')

Ďalšou našou úlohou bude pripraviť triedu Plocha tak, aby vedela pracovať aj s inými animovanými obrázkami ako 'vtak.png'. Napríklad v súbore 'zajo.png' máme týchto 8 fáz animácie:

_images/23_2.png

Teda trieda Plocha nebude sama čítať a rozoberať súbor 'vtak.png', ale takéto zoznamy animácií pripravíme ešte pred inicializáciou Plocha a sem ich pošleme, napríklad v parametri obrazky. Vďaka tomuto budeme môcť do plochy poslať ľubovoľný počet zoznamov s animáciami. Upravíme aj výber animácie pri kliknutí:

import tkinter
import random
from PIL import Image, ImageTk

class Plocha:
    def __init__(self, meno_pozadia, *obrazky):
        self.pozadie = tkinter.PhotoImage(file=meno_pozadia)
        sir, vys = self.pozadie.width(), self.pozadie.height()
        self.canvas = Anim.canvas = tkinter.Canvas(width=sir, height=vys)
        self.canvas.pack()
        self.canvas.create_image(0, 0, image=self.pozadie, anchor='nw')
        self.zoz = obrazky
        self.azoz = []
        self.timer()
        self.canvas.bind('<ButtonPress>', self.klik)

    def timer(self):
        for a in self.azoz:
            a.dalsia_faza()
        self.canvas.after(100, self.timer)

    def klik(self, event):
        self.azoz.append(Anim(event.x, event.y, random.choice(self.zoz)))

a hlavný program:

win = tkinter.Tk()
Plocha('les.png', strihaj('vtak.png', 8), strihaj('zajo.png', 8))

Táto grafická aplikácia vykreslí do pozadia obrázok lesa a každé kliknutie pridá animovaného vtáčika alebo zajaca.

Pomocou PIL vytvoríme aj ďalšie animácie:

win = tkinter.Tk()
win.title('zvieratka v lese')
zoz1 = strihaj('vtak.png', 8)
zoz2 = strihaj('zajo.png', 8)
obr = Image.open('pyton.png')
zoz3 = [ImageTk.PhotoImage(obr.rotate(uhol, expand=True)) for uhol in range(0, 360, 10)]
obr = Image.open('kacicka.png')
zoz4 = [ImageTk.PhotoImage(obr.resize(int(v*r) for v in obr.size))
                           for r in (.6, .55, .5, .45, .4, .35, .3, .35, .4, .45, .5, .55)]
Plocha('les.png', zoz1, zoz2, zoz3, zoz4)

Aby sme nemali v našej grafickej aplikácii zbytočne veľa globálnych premenných, zapuzdrime to do triedy Program:

import tkinter
import random
from PIL import Image, ImageTk

class Plocha:
    ...

class Anim:
    canvas = None
    ...

class Program:
    def __init__(self):

        def strihaj(meno_suboru, n):
            obr = Image.open(meno_suboru)
            sir, vys = obr.width//n, obr.height
            zoz = []
            for x in range(0, obr.width, sir):
                zoz.append(ImageTk.PhotoImage(obr.crop((x, 0, x+sir, vys))))
            return zoz

        win = tkinter.Tk()
        win.title('zvieratka v lese')
        zoz1 = strihaj('vtak.png', 8)
        zoz2 = strihaj('zajo.png', 8)
        obr = Image.open('pyton.png')
        zoz3 = [ImageTk.PhotoImage(obr.rotate(uhol, expand=True)) for uhol in range(0, 360, 10)]
        obr = Image.open('kacicka.png')
        zoz4 = [ImageTk.PhotoImage(obr.resize(int(v*r) for v in obr.size))
                                   for r in (.6, .55, .5, .45, .4, .35, .3, .35, .4, .45, .5, .55)]
        Plocha('les.png', zoz1, zoz2, zoz3, zoz4)

Program()

Teraz bude grafická aplikácia náhodne vyberať jeden z animovaných obrázkov vtáčika, zajaca, hada a kačičky. V celej aplikácii máme okrem troch tried Plocha, Anim a Program jednu globálnu inštanciu triedy Program.


Cvičenia

L.I.S.T.


  1. Spojazdni kompletnú aplikáciu z prednášky.


  1. Vymeň v aplikácii pozadie: nájdi na internete vhodný obrázok rozmerov aspoň 800x600 vo formáte najlepšie .jpg a nahraď ním 'les.png'.


  1. V priečinku obrazky2.zip je okrem niekoľkých obrázkov aj podpriečinok pozadie. Zvoľ si niektorý z nich a ten rozkopíruješ vedľa seba a pod seba tak, aby sa zaplnil celý canvas veľkosti minimálne 800x600.

    • pomocou Image vytvor jeden veľký obrázok požadovaných rozmerov (napríklad 800x600) a do neho príslušný počet krát opečiatkuj jeden z obrázkov a tento výsledok použi ako pozadie canvasu grafickej aplikácie

    • mohla by teraz fungovať, napríklad takáto inicializácia triedy Plocha

    Plocha(('pozadie/pozadie1.bmp', 800, 600), zoz1, zoz2, zoz3, zoz4)
    

  1. Obrázky 'obr1.bmp' a 'obr2.bmp' nemajú priesvitné časti. Prečítaj ich a pomocou Image z nich vyrob obrázky v móde 'RGBA' a farbu v pixeli na súradnici (0, 0) nahraď v týchto obrázkoch priesvitnými pixelmi.

    • pre každú z týchto bitmáp priprav 2 fázy animácie: 1. je pôvodný obrázok, 2. je obrázok zmenšený na 90%


  1. Napíš funkciu strihaj_gif(meno_suboru) (podobnej funkcii z 21. cvičení), ktorý prečíta gif súbor a vyrobí z neho zoznam obrázkov pre tkinter. Otestuj túto funkciu pre súbory 'kraca.gif' a 'potvorka.gif' a zaraď ich do zoznamu obrázkov do aplikácie.


  1. Trieda Plocha očakáva ako zbalený parameter obrazky len zoznamy animácií, t.j. zoznamy obrázkov pre tkinter. Doplň do tejto inicializácie aj to, aby fungovali nielen takéto zoznamy, ale aj znakové reťazce. Tieto reťazce sú menami súborov vo formáte png. Napríklad by mohlo fungovať aj:

    Plocha('les.png', zoz1, zoz2, 'konik.png', zoz3, zoz4, 'psicek.png')
    

    Takéto súbory sa tiež prečítajú (pomocou tkinter.PgotoImage) a zaradia medzi zoznamy animácií ako animácie s jedinou fázou.


12. Týždenný projekt

L.I.S.T.


Robot Mravec

Máme cvičeného malého robota mravca, ktorý sa pohybuje po štvorcovej sieti a pritom vie pred sebou tlačiť malé kocky. Mravec poslúcha na povely 'l' (vľavo), 'p' (vpravo), 'h' (hore), 'd' (dole), pričom sa v danom smere posunie na susedné políčko štvorcovej siete. Ak sa v danom smere v ploche nachádza kocka, tak ju pred sebou v tomto smere potlačí. Ak je v danom smere tesne za sebou viac kociek, tak ich tlačí všetky. Mravec z plochy nikdy nevyjde, hoci kocky, ktoré pred sebou tlačí, z plochy vypadnúť môžu.

Na každej kocke je zapísané jedno písmeno. Samotná štvorcová sieť vie indikovať, či sa na niektorých špeciálnych políčkach nachádzajú kocky s písmenami a vie zistiť množinu písmen na týchto kockách.

Zadanie štvorcovej siete s počiatočným rozložením kociek je v textovom súbore. V prvých riadkoch tohto súboru sa nachádzajú riadky štvorcovej siete (všetky sú rovnako dlhé), pričom špeciálne políčka sú označené znakom '+' a zvyšné sú označené znakom '.'. Za štvorcovou sieťou je v súbore jeden riadok prázdny a za tým nasleduje postupnosť súradníc kociek s písmenami - v každom ďalšom riadku je trojica: písmeno a dve celé čísla. Táto trojica označuje písmeno na kocke a jej pozíciu v ploche: riadok a stĺpec (číslujeme od 0).

Naprogramuj triedu Mravec:

class Mravec:
    def __init__(self, meno_suboru):
        ...

    def __str__(self):
        return ''

    def start(self, riadok, stlpec):
        ...

    def rob(self, prikazy):
        ...

    def zisti(self):
        return set()

kde

  • init prečíta súbor - mravec tam zatiaľ nie je

  • start položí mravca na zadaný riadok a stĺpec

  • __str__ vráti znakovú reprezentáciu plochy: pozíciu mravca zapíšte znakom '@' a špeciálne políčka, na ktorých sa nenachádza ani mravec ani kocka, zapíšte znakom '+', ostatné políčka zapíšte znakmi písmen, resp. znakom '.'

  • rob dostáva jeden povel, alebo postupnosť za sebou nasledujúcich povelov, pričom povel je jedno z písmen 'l', 'p', 'h' alebo 'd'; mravec sa postupne pohybuje v daných smeroch, pričom pred sebou môže tlačiť kocky; povely, ktoré sa nedajú vykonať, ignoruje

  • metóda zisti vráti množinu písmen na špeciálnych políčkach hracej plochy

Napríklad, pre súbor:

.....
..+.+
.++..
.....

D 2 2
C 2 1
A 1 1
B 1 3

takýto test:

if __name__ == '__main__':
    m = Mravec('subor1.txt')
    print(m)
    print('zisti =', m.zisti())
    m.start(1, 0)
    m.rob('pp')
    print(m)
    print('zisti =', m.zisti())
    m.rob('dl')
    print(m)
    print('zisti =', m.zisti())

vypíše:

.....
.A+B+
.CD..
.....
zisti = {'D', 'C'}
.....
..@AB
.CD..
.....
zisti = {'B', 'D', 'C'}
.....
..+AB
C@+..
..D..
zisti = {'B'}

Z úlohového servera L.I.S.T. si stiahni kostru programu riesenie.py. Pozri si testovacie dáta v súboroch 'subor1.txt', 'subor2.txt', 'subor3.txt', …, ktoré bude používať testovač.

Tvoj odovzdaný program s menom riesenie.py musí začínať tromi riadkami komentárov:

# 12. zadanie: mravec
# autor: Janko Hraško
# datum: 17.12.2020

Projekt riesenie.py (bez dátových súborov) odovzdaj na úlohový server https://list.fmph.uniba.sk/ najneskôr do 23:00 30. decembra, kde ho môžeš nechať otestovať. Testovač bude spúšťať metódy triedy s rôznymi vstupmi. Odovzdať projekt aj ho testovať môžeš ľubovoľný počet krát. Môžeš zaň získať 5 bodov.