22. Animované obrázky


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/22_1.png

Na konci predchádzajúcej prednášky sme takáto súbor najprv prečítali, rozstrihali a uložili do zoznamu a potom sme otestovali:

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 minulých cvičení. V tejto verzii funkcia dostáva meno súboru, ktorý treba rozstrihať a vytvorí zoznam obrázkov ale prekonvertované do formátu 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ť len pri 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/22_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 .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 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 minulých 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.


10. Domáce zadanie

L.I.S.T.


Robot Karel

Robot Karel sa pohybuje po štvorcovej sieti, v ktorej sa na niektorých políčach nachádzajú kartičky s nejakými symbolmi. Robot prechádza ponad tieto políčka, pričom na niektorých môže kartičku pod sebou zdvihnúť (vloží si ju do svojho batoha), resp. karičku z batoha vybrať a položiť na políčko pod seba. Robot reaguje na povely 'vlavo', 'vpravo', 'krok', 'zdvihni', 'poloz':

  • robot je natočený v jednom zo štyroch smerov, označovať ich budeme takto: 0 na východ, 1 na juh, 2 na západ, 3 na sever

  • príkazy 'vlavo', resp. 'vpravo' otočia robota v danom smere

  • príkazom 'krok' robot prejde v momentálnom smere na susedné políčko, ak je už na okraji siete, z plochy nevypadne, ale v danom smere nevykoná nič

  • príkazom 'zdvihni' zoberie kartičku z políčka pod sebou a vloží ju do batoha; ak na danom políčku nebola žiadna kartička, príkaz nevykoná nič; ak na danom políčku bolo na sebe viac kartičiek, robot zdvihne najvrchnejšiu z nich; kartičky vkladá do batoha na seba v poradí ako ich zdvíhal z plochy (naspodu je prvá, na vrchu posledne zdvihnutá)

  • príkazom 'poloz' vyberie najvrchnejšiu kartičku z batoha a vloží ju na políčko pod seba; ak bol batoh prázdny, príkaz neurobí nič; ak na políčku už boli nejaké kartičky pred tým, novú kartičku položí na vrch týchto kartičiek.

Zadanie štvorcovej siete s počiatočným rozložením kartičiek je v textovom súbore. V prvom riadku je dvojica celých čísel, ktorá popisuje veľkosť štvorcovej siete: počet riadkov a počet stĺpcov. Za tým nasleduje informácia o kartičkách v ploche - v každom riadku je symbol na kartičke a dvojica celých čísel, ktoré označujú riadok a stĺpec pozície kartičky (číslujeme od 0). Na jednom políčku sa môže nachádzať aj viac kartičiek.

Naprogramuj triedu RobotKarel:

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

    def __str__(self):
        return ''

    def robot(self, riadok, stlpec, smer):
        ...

    def rob(self, prikaz):
        return 0

    def batoh(self):
        return []

kde

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

  • __str__ vráti znakovú reprezentáciu plochy: pozíciu robota zapíš (podľa momentálneho natočenia) jedným zo znakov '>', 'v', '<', '^', ak je na políčku viac kartičiek, zobrazí sa iba najvrchnejšia z nich, prázdne políčko zobraz znakom '.'; ak je robot na políčku s kartičkami, zobrazí sa iba robot

  • robot položí robota na zadaný riadok a stĺpec s daným otočením (číslo od 0 do 3)

  • rob dostáva jeden povel, alebo postupnosť za sebou nasledujúcich povelov, pričom povel je jeden z reťazcov 'vlavo', 'vpravo', 'krok', 'zdvihni', 'poloz', ktorý môže mať na začiatku aj celé číslo, vtedy to označuje počet opakovaní; napr. '3 krok' označuje tri kroky za sebou; robot sa postupne pohybuje v danom smere, pričom zbiera, resp. kladie kartičky; povely, ktoré sa nedajú vykonať, ignoruje; funkcia vráti počet tu vykonaných ale neignorovaných povelov

  • metóda batoh vráti momentálny zoznam kartičiek so symbolmi v batohu (prvým prvkom je najspodnejšia kartička, posledným je najvrchnejšia)

  • odporúčame štvorcovú sieť reprezentovať ako dvojrozmernú tabuľku (zoznam zoznamov), v ktorej každý prvok je buď reťazec (postupnosť znakov) alebo zoznam znakov, políčka bez kartičiek reprezentuj prázdnym reťazcom, resp. zoznamom

Napr. pre súbor 'subor1.txt':

3 4
N 1 3
O 1 2
H 1 1
P 0 1
Y 0 2
T 0 3

takýto test:

if __name__ == '__main__':
    k = RobotKarel('subor1.txt')
    k.robot(0, 0, 0)
    print(k)
    print(k.rob('krok'))
    print(k.rob('2 zdvihni'))
    k.rob('krok')
    k.rob('vpravo')
    k.rob('krok')
    k.rob('2 zdvihni')
    k.rob('2 krok')
    print(k)
    print('batoh =', k.batoh())
    k.rob('poloz vlavo')
    k.rob('krok 6 vlavo')
    print(k)
    print('batoh =', k.batoh())

vypíše:

>PYT
.HON
....
1
1
..YT
.H.N
..v.
batoh = ['P', 'O']
..YT
.H.N
..O<
batoh = ['P']

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:

# 10. zadanie: karel
# autor: Janko Hraško
# datum: 17.12.2019

Riešenie (bez dátových súborov) odovzdaj na úlohový server https://list.fmph.uniba.sk/ najneskôr do 23:00 27. decembra, kde ho môžeš nechať otestovať. Testovač bude spúšťať metódy triedy s rôznymi vstupmi. Môžeš zaň získať 10 bodov.