21. Práca s obrázkami

Python Imaging Library

Aby ste mohli pracovať s knižnicou PIL (Python Imaging Library), musíte mať nainštalovaný modul Pillow. Základné info o inštalácii nájdete na stránke Inštalácia Pillow. Pod operačným systémom Windows (podobne aj v iných OS) treba v príkazovom okne cmd spustiť príkaz:

> pip install Pillow

Je možné, že bude od vás požadovať administrátorské práva, resp. spustiť cmd ako správca.

Po úspešnej inštalácii musíme v samotnom Pythone na začiatok našich programov zadať:

from PIL import Image

Teraz je Python pripravený pracovať s obrázkovými objektmi. Základné informácie o tomto module môžete nájsť na webe Pillow.

Vytvorenie obrázkového objektu

Knižnica Image umožňuje pracovať s rastrovými obrázkami priamo v pamäti. Obrázkové objekty (inštancie triedy Image.Image) sa vytvárajú buď prečítaním obrázkového súboru, alebo vytvorením nového obrázka (jednofarebný obdĺžnik) alebo ako výsledok niektorých operácií s nejakými už existujúcimi obrázkami.

Obrázkový objekt vytvoríme prečítaním obrázkového súboru z disku pomocou príkazu:

>>> obr = Image.open('meno súboru')

Obrázkový súbor môže mať skoro ľubovoľný grafický typ, napr. png, bmp, jpg, gif, … Aby tento súbor príkaz Image.open() mal šancu nájsť, buď sa bude nachádza v tom istom priečinku na disku ako náš program, alebo mu zadáme relatívnu alebo absolútnu cestu, napr.

>>> obr1 = Image.open('tiger.bmp')
>>> obr2 = Image.open('obrazky/slon.jpg')
>>> obr3 = Image.open('d:/user/data/python.png')

Po prečítaní súboru z danej inštancie môžeme zistiť niektoré jeho parametre, napr.

>>> obr1
<PIL.BmpImagePlugin.BmpImageFile image mode=RGB size=384x256 at 0x2C4F710>
>>> obr1.size
(384, 256)
>>> obr1.width
384
>>> obr1.height
256
>>> obr1.mode
'RGB'

Obrázky môžu byť uložené v niekoľkých rôznych módoch: pre nás budú zaujímavé len dva z nich 'RGB' a 'RGBA' pre „obyčajné“ rgb a rgb, ktoré si pamätá aj priesvitnosť, tzv. alfa-kanál, teda rgba.

Veľmi často používaným príkazom tri testovaní a ladení bude:

>>> obr1.show()

ktorý zobrazí momentálny obsah nami pripravovaného obrázku v nejakom externom programe - toto závisí od nastavení vo vašom operačnom systéme.

Obrázkový objekt môžeme vytvoriť aj pomocou funkcie new(), ktorej zadáme „mód“ (t.j. 'RGB' alebo 'RGBA'), veľkosť obrázka v pixeloch ako dvojicu (šírka, výška) a prípadne farbu, ktorou sa tento obrázok zafarbí (inak bude čierny):

>>> obr4 = Image.new('RGB', (300, 200), 'pink')

Vytvorí nový objekt obr4 - obrázok veľkosti 300x200 pixelov (šírka 300, výška 200), ktorý bude celý ružový. Ako farbu pixelov tu môžeme okrem mena farby uviesť aj reťazec známy z tkinter, ktorý obsahuje cifry v 16-ovej sústave, napr. '#12a4ff', alebo trojicu rgb (teda tuple) v tvare, napr. (120, 198, 255). Neskôr tu uvidíme aj štvoricu pre rgba.

Ak by sme chceli vytvoriť nový obrázok rovnakej veľkosti akú má niektorý už existujúci, môžeme využiť jeho atribút size, napr.

>>> obr4 = Image.new('RGB', obr1.size, '#ffffff')

vytvorí obrázkový objekt eľkosti 384x256, pričom všetky jeho pixely budú biele.

Uloženie obrázka do súboru

Metóda save('meno súboru') uloží obrázok do súboru s ľubovoľnou príponou. Takto môžeme veľmi ľahko zmeniť grafický formát obrázku. Napr.

>>> obr1.save('tiger.png')
>>> obr4.save('temp/prazdny.jpg')

Obrázok obr1, ktorý mal pôvodne bitmapový formát (prípona .bmp) sa zapísal do .png súboru. Obrázok obr4, ktorý sme vytvorili pomocou metódy new sme zapísali do .jpg súboru.

Uvedomte si, že ak nejaký obrázok chceme len prečítať a hneď ho zapísať do súboru (možno s inou príponou), môžeme to priamo zapísať:

>>> Image.open('obrazok.jpg').save('obrazok.png')

Vytvorila sa kópia pôvodného obrázka s novou prípomnou.

Oblasť v obrázku

Z obrázka môžeme odkopírovať ľubovoľnú obdĺžnikovú oblasť a uložiť ju do iného obrázka. Oblasť definujeme ako štvoricu (tuple) štyroch celých čísel (x, y, x+šírka, y+výška), kde (x, y) je poloha ľavého horného pixla oblasti (riadky aj stĺpce indexujeme od 0) a šírka a výška sú rozmery oblasti v pixeloch, t.j. počet stĺpcov a riadkov pixelov.

Na vykopírovanie oblasti slúži príkaz:

>>> novy_obr = povodny_obr.crop(oblasť)

Touto metódou vzniká nový obrázok zadaných rozmerov (podľa oblasti) s vykopírovaným obsahom. Pôvodný obrázok ostáva bez zmeny. Ak je časť oblasti mimo pôvodného obrázka, v novom obrázku tu budú doplnené čierne (resp. pre rgba priesvitné) pixely.

Napr.

>>> obr1a = obr1.crop((60, 50, 220, 200))
>>> obr1a.size
(160, 150)

Ak vieme pozíciu (x, y) ľavého horného pixelu oblasti a jej šírku sir a výšku vys, môžeme kopírovanie zapísať aj takto:

>>> x, y, sir, vys = 60, 50, 160, 150
>>> obr1a = obr1.crop((x, y, x + sir, y + vys))

niekedy môžete vidieť aj takýto zápis:

>>> oblast = 60, 50, 220, 200
>>> obr1a = obr1.crop(oblast)

Pomocou metódy crop() sme vytvorili nový obrázkový objekt obr1a. Na nasledovnom obrázku vidíme pôvodný obrázok aj nový s kópiou jeho časti (oblasti):

_images/21_1.png

Ďalším príkazom je metóda paste(), pomocou ktorej sa vloží obrázok na nejaké miesto iného obrázka:

>>> obr1.paste(obr2, kam)

Tento príkaz modifikuje pôvodný obsah obrázka obr1. Obrázok obr2 sa „opečiatkuje“ do obr1, pričom parameter kam určí pozíciu. Najvhodnejšie je sem písať dvojicu (x, y) t.j. pozícia v obr1, kam sa umiestni obr2, t.j. jeho ľavý horný pixel (inak 4-prvkový parameter kam musí určovať rozmer, ktorý je identický s rozmerom obr2). Uvedomte si, že táto metóda nevracia žiadnu hodnotu (teda vráti None) a preto nemá zmysel jej výsledok priraďovať do nejakej premennej.

Pozor na „lazy“ vyhodnocovanie

Príkaz crop(), ktorý vykopíruje časť obrázka a vytvorí z neho nový obrázok má tzv. lazy vyhodnocovanie. To znamená, že hoci samotná operácia ešte neskončila, Python začne vyhodnocovať ďalší príkaz programu. Väčšinou to nevadí, ale niekedy, ak v ďalšom príkaze potrebujeme pracovať s obrázkovou premennou s vykopírovaným obsahom (napr. v paste()), samotný crop() ešte nemusel dobehnúť. Z tohto dôvodu sa ešte pred paste() odporúča presvedčiť, že rozbehnutý crop() už skončil. Na to slúži príkaz obr.load(), napr.

maly = velky.crop(oblast)
maly.load()                 # tu sa počká na dokončenie crop()
velky.paste(maly, (x, y))

Príkaz paste() má aj iné využitie:

>>> obr1.paste(pixel, oblasť)

V tomto prípade je pixel nejakou farbou a táto sa vyleje do špecifikovanej oblasti (nakreslí zafarbený obdĺžnik). Napr.

img = Image.new('RGB', (300, 200), 'gray')
img.paste('red', (20, 20, 80, 180))
img.paste((0, 0, 255), (100, 20, 160, 180))
img.paste('#bf00bf', (180, 20, 240, 180))

Keď budete s príkazom paste() experimentovať, môžete si pomocou príkazu copy() vytvárať kópiu pôvodného obrázka, keďže paste() ho modifikuje. Napr.

>>> zaloha = obr1.copy()
>>> obr1.paste('yellow', (50, 40, 230, 210))
>>> obr1.paste(obr1a, (60, 50))
>>> obr1.show()     # alebo obr1.save(...)
>>> obr1 = zaloha

Zobrazí sa tento obrázok:

_images/21_2.png

A premenná obr1 bude stále obsahovať nepokazený obrázok tigra.

Premysleným strihaním nejakého obrázka a potom skladaním týchto rozstrihaných častí:

from PIL import Image

obr1 = Image.open('tiger.bmp')
d = 10
novy = Image.new('RGB', (384 + 7*d, 256 + 5*d), 'pink')
for i in range(6):
    for j in range(4):
        x, y = i * 64, j * 64
        novy.paste(obr1.crop((x, y, x + 64, y + 64)), (x + i*d + d, y + j*d + d))
novy.show()

môžeme dostať:

_images/21_3.png

Zmeny obrázka

Obrázku vieme zmeniť veľkosť, napr.

>>> obr2 = obr1.resize((nova_sirka, nova_vyska))

Vytvorí sa nový obrázok so zadanými rozmermi, pôvodný ostáva bez zmeny. Napr.

>>> obr3 = obr1.resize((obr1.width*3, obr1.height*3))
>>> obr1 = obr1.resize((obr1.width//2, obr1.height//2))

Obrázok obr3 má trojnásobné rozmery pôvodného obrázka, pričom obr1 sa zmenší na polovičné rozmery. Napríklad, ak máme takýto obrázok 'prasiatko.png':

_images/21_4.png

keď mu zmeníme veľkosť bez zachovania pomeru strán:

obr1 = Image.open('prasiatko.png')
obr2 = obr1.resize((300, 100))

dostávame:

_images/21_5.png

ale zmenšenie oboch strán na tretinu:

sirka, vyska = obr1.size
obr3 = obr1.resize((sirka // 3, vyska // 3))

vyzerá takto:

_images/21_6.png

Uvedomte si, že zmenšovaním obrázka sa nejaká informácia stráca, takže, keď mu po zmenšení vrátime pôvodnú veľkosť:

obr4 = obr1.resize((sirka // 10, vyska // 10)).resize(obr1.size)

dostávame:

_images/21_7.png

Obrázok môžeme otáčať, resp. preklápať pomocou transpose():

>>> novy_obr = obr1.transpose(Image.FLIP_LEFT_RIGHT)     # preklopí
>>> novy_obr = obr1.transpose(Image.FLIP_TOP_BOTTOM)     # preklopí
>>> novy_obr = obr1.transpose(Image.ROTATE_90)           # otočí
>>> novy_obr = obr1.transpose(Image.ROTATE_180)          # otočí
>>> novy_obr = obr1.transpose(Image.ROTATE_270)          # otočí

Zrejme sa pri tomto niekedy zmenia rozmery výsledného obrázka.

Otáčať môžeme aj o ľubovoľný uhol:

>>> novy_obr = obr1.rotate(uhol)

Uhol zadávame v stupňoch v protismere otáčania hodinových ručičiek. Ak otočíme obrázok prasiatka obr1 napr. o 30 stupňov:

obr5 = obr1.rotate(30)

dostávame:

_images/21_8.png

Výsledný obrázok bude mať pôvodné rozmery obr1, teda nejaké otočené časti sa pritom stratia a ešte pribudli aj nejaké čierne oblasti. Ak ale zadáme:

obr6 = obr1.rotate(30, expand=True)

výsledný obrázok sa zväčší tak, aby sa teraz do neho zmestil celý otočený pôvodný obrázok. Teraz je ešte lepšie vidieť nové čierne oblasti:

_images/21_9.png

Práca s jednotlivými pixelmi

Metóda getpixel() vráti farbu konkrétneho pixelu, napr.

>>> obr1.getpixel((108, 154))
(228, 187, 122)

Metóda putpixel() zmení konkrétny pixel v obrázku. Napr.

>>> obr1.putpixel((108, 154), (255, 0, 0))

Zafarbí daný pixel na červeno. V tomto prípade musí byť farba zadaná ako trojica (resp. pre rgba ako štvorica) čísel od 0 do 255. Ak v obrázku prasiatka zafarbíme v nejakej oblasti 500 náhodných bodiek:

from random import randrange as rr

for i in range(500):
    xy = rr(200, 300), rr(50, 150)
    farba = (255, 0, 0)
    obr1.putpixel(xy, farba)

vyzerá to nejako takto:

_images/21_10.png

Nalseovný zápis (generátorová notácia) zistí množinu všetkých farieb v obrázku:

>>> mn = {obr1.getpixel((x, y)) for x in range(obr1.width) for y in range(obr1.height)}
>>> len(mn)
17653

Priesvitnosť

Obrázok musí byť v móde 'RGBA', t.j. každý pixel je štvorica celých čísel, pričom oproti 'RGB' obsahuje navyše ešte jednu číselnú informáciu o priesvitnosti (tzv. alfa-kanál). Pre toto číslo bude 255 označovať nepriesvitný pixel, 0 úplne priesvitný pixel (vtedy na farbe rgb nezáleží), a hodnoty medzi tým označujú mieru priesvitnosti.

Pomocou metódy convert() môžeme prekonvertovať mód obrázka, napr.

>>> im = Image.open('obrazok.bmp')
>>> im1 = im.convert('RGBA')
>>> im2 = Image.open('obrazok2.png').convert('RGBA')

Obrázok im má zachovaný mód zo súboru, obrázky im1 aj im2 majú zmenený mód na 'RGBA'. Pre tieto obrázky musíme odteraz pixely zadávať ako štvorice (r, g, b, a), operácie crop() aj rotate() môžu vytvoriť priesvitné pixely za hranicou pôvodných obrázkov. Operácia paste() ale správne nezlučuje priesvitné pixely s pôvodným obrázkom, tak ako by sme očakávali. Na to potrebujeme iný mechanizmus:

  • funkcia alpha_composite() dokáže na seba položiť dva obrázky, ktoré majú priesvitnosť (polopriesvitnosť)

  • jej formát je Image.alpha_composite(obr1, obr2), kde oba obrázky musia mať rovnaké rozmery a mód 'RGBA', výsledkom je nový obrázok, v ktorom je na obr1 položený obr2, pričom cez priesvitné časti obr2 sú vidieť pixely z obr1

Tento mechanizmus môžete vidieť vo funkcii poloz():

def poloz(obr_kam, obr_co, xy):
    w, h = obr_co.size
    x, y = xy
    box = (x, y, x + w, y + h)
    obr_kam.paste(Image.alpha_composite(obr_kam.crop(box), obr_co), box)

Funkcia do obrázka obr_kam položí obr_co na pozíciu xy (čo je dvojica (x, y)), xy je pozícia, kam sa dostane ľavý horný pixel obr2 v obr1. Táto pozícia xy sa môže nachádzať aj mimo obr1.

Napr. tento kód s prasiatkom a obrázkom mašličky:

from random import randrange as rr

obr1 = Image.open('prasiatko.png').convert('RGBA')
masla = Image.open('masla.png')             # mala by byt RGBA
for i in range(10):
    xy = rr(50, 350), rr(50, 200)
    poloz(obr1, masla, xy)

vytvorí takýto obrázok:

_images/21_11.png

Rozoberanie animovaných gif

Animovaný gif súbor sa skladá zo série za sebou idúcich obrázkov. Načítanie takéhoto súboru pomocou Image.open() nám automaticky sprístupní prvú fázu (má poradové číslo 0). Ak potrebujeme pracovať s i-tou fázou animácie (i-tym obrázkom série), použijeme metódu obr.seek(i).

Nasledujúca časť programu otvorí obrázkový súbor s animáciou napr. 'vtak.gif', ktorý sa skladá z neznámeho počtu obrázkov. Postupne všetky tieto fázy uloží do samostatných obrázkových súborov vo formáte 'png':

gif = Image.open('vtak.gif')
i = 0
while True:
    gif.save(f'vtak/vtak{i}.png')
    try:
        i += 1
        gif.seek(i)
    except EOFError:
        break

Pre tento súbor 'vtak.gif' s animovaným obrázkom:

_images/21_12.gif

dostaneme v priečinku vtak túto postupnosť súborov:

_images/21_13.png

Vypĺňanie oblasti farbou

Na vypĺňanie farbou nejakej oblasti, ktorá je ohraničená nejakým obrysom, slúži funkcia floodfill() z modulu ImageDraw. Funkcia má tieto parametre (dva varianty volania):

from PIL import ImageDraw

ImageDraw.floodfill(obr, xy, farba)
ImageDraw.floodfill(obr, xy, farba, hraničná_farba)

kde

  • obr je obrázok, v ktorom sa bude vyfarbovať nejaká oblasť

  • xy je dvojica (x, y) pozície v obrázku, kde sa naštartuje „vylievanie“ farby

  • farba je tá farba, ktorou sa bude vyfarbovať

  • hraničná_farba - ak nie je zadaná, tak sa farba vylieva do celej súvislej oblasti, ktorá má rovnakú farbu ako bod na pozícii xy; ak je tento 4 parameter zadaný, tak hranicu súvislej oblasti určujú pixely tejto farby

Nasledovná ukážka demonštruje použitie tejto funkcie:

from PIL import Image, ImageDraw
from random import randrange as rr

f = Image.open('fill.png')
for i in range(100):
    xy = rr(f.width), rr(f.height)
    if f.getpixel(xy) == (255, 255, 255):
        ImageDraw.floodfill(f, xy, (rr(256), rr(256), rr(256)))

f.show()

V pôvodnom obrázku 'fill.png' sú niektoré plochy biele. Tento program stokrát náhodne zvolí nejakú pozíciu a ak je tento pixel biely, tak vyplní celú súvislú oblasť s náhodnou farbou.

Ak bol súbor 'fill.png' takýto:

_images/21_14.png

môžeme dostať napríklad tento obrázok:

_images/21_15.png


Cvičenia

L.I.S.T.

  1. Napíš funkciu novy(sir, vys, meno_suboru=None), ktorá vytvorí biely obrázok veľkosti sir x vys a ak je zadané aj meno_suboru, uloží ho do tohto súboru, inak vráti obrázok ako výsledok funkcie. Pomocou tejto funkcie potom vytvor súbor 'biely.bmp' s bitmapou veľkosti 100x100. Skontroluj to na disku.

    • napr.

      >>> novy(600, 100).show()
      

  1. Napíš funkciu konvertuj(meno_suboru1, meno_suboru2), ktorá prekonvertuje súbor meno_suboru1 na súbor meno_suboru2. Nájdi na internete obrázok vo formáte '.bmp', ulož ho na disk a potom pomocou funkcie konvertuj() ho prekonvertuj na '.png' formát.

    • napr.

      >>> konvertuj('obrazok.bmp', 'obrazok.png')
      

  1. Vytvor biely obrázok veľkosti 380x380, do ktorého vložíš 16 farebných štvorcov veľkosti 80x80 (medzi štvorcami je medzera 20 pixelov). Farby štvorcov si zvoľ ľubovoľne (napr. všetky sú rovnaké, alebo sa nejaké striedajú, alebo sú náhodné). Obrázok ulož do súboru.


  1. Napíš funkciu zmensi(obrazok), ktorá zmenší daný obrázok tak, že jeho šírka bude 128 a výška sa prepočíta tak, aby bol zachovaný pomer strán. Funkcia ako výsledok vráti tento zmenšený obrázok. Prečítaj nejaký obrázok zo súboru a pomocou zmensi() ho zmenši a ulož do súboru s pozmeneným menom. Skontroluj výsledný súbor.

    • napr.

      >>> zmensi(Image.open('subor1.png')).save('subor1x.png')
      

  1. Napíš funkciu vymen(obrázok), ktorá navzájom v obrázku vymení ľavú a pravú polovicu obrázku. Funkcia nemodifikuje pôvodný obrázok, ale vráti nový s vymenenými polovicami. Prečítaj nejaký obrázok zo súboru a pomocou vymen() vytvor nový a ten ulož do súboru. Výsledok skontroluj.

    • napr.

      >>> vymen(Image.open('subor2.png')).save('subor2x.png')
      

  1. Napíš funkciu kopia(obrazok), ktorá vyrobí kópiu pôvodného obrázka, ale ho kopíruje po jednom pixeli. Zrejme si najprv vytvoríš prázdny obrázok rovnakých rozmerov a sem budeš kopírovať pixely (pomocou getpixel() a putpixel()). Funkcia vráti tento nový obrázok ako svoj výsledok. Teraz prečítaj nejaký malý obrázok zo súboru a vyrob z neho pomocou kopia() kópiu. Výsledok ulož do súboru a skontroluj.

    • napr.

      >>> kopia(obr1).show()
      

  1. Napíš funkciu prevrat(obrazok), ktorá vyrobí prevrátenú kópiu pôvodného obrázka (obrázok je hore nohami), ale ho kopíruje po jednom pixeli. Funkcia vráti tento nový obrázok ako svoj výsledok. Teraz prečítaj nejaký malý obrázok zo súboru a vyrob z neho nový pomocou prevrat(). Výsledok ulož do súboru a skontroluj.

    • napr.

      >>> prevrat(obr1).show()
      

  1. Napíš funkciu sedy(obrazok), ktorá vyrobí čierno-bielu kópiu pôvodného obrázka (vypočítate priemer zložiek (r, g, b) (nech je to p) a z toho vznikne nová farba (p, p, p)). Funkcia vráti tento nový obrázok ako svoj výsledok. Teraz prečítaj nejaký malý obrázok zo súboru a vyrob z neho čiernobiely pomocou sedy(). Výsledok ulož do súboru a skontroluj.

    • napr.

      >>> sedy(obr1).show()
      

  1. Napíš funkciu strihaj1(obr, n), ktorá rozstrihá zadaný obrázok na n rovnako-širokých častí (po stĺpcoch) a všetky takto rozstrihané časti vráti ako zoznam obrázkov.


  1. Napíš funkciu strihaj2(obr, n), ktorá rozstrihá zadaný obrázok na n rovnako-vysokých častí (po riadkoch) a všetky takto rozstrihané časti vráti ako zoznam obrázkov.


  1. Napíš funkciu zlep1(zoznam), ktorá zlepí vedľa seba obrázky zadané v zozname (napr. sú výsledkom strihaj1()). Výsledný obrázok vráti ako výsledok funkcie.

    • otestujte:

      >>> zlep1(strihaj1(obr1, 5)[::-1]).show()
      

  1. Napíš funkciu zlep2(zoznam), ktorá zlepí pod seba obrázky zadané v zozname (napr. sú výsledkom strihaj2()). Výsledný obrázok vráti ako výsledok funkcie.


  1. Napíš funkciu zapis(zoznam, meno, pripona), ktorá v parametri zoznam dostáva postupnosť obrázkov a všetky tieto obrázky uloží do súborov s menami ‚meno0.pripona‘, ‚meno1.pripona‘, ‚meno2.pripona‘, …


  1. Napíš funkciu citaj(*mena_suborov), ktorá ako parameter dostáva postupnosť mien grafických súborov, tieto súbory prečíta, uloží do zoznamu a tento nový zoznam vráti ako výsledok funkcie.

    • napr.

      >>> zoznam = citaj('tiger.bmp', 'image0.png', 'image1.png')
      
    • vytvorí trojprvkový zoznam prečítaných obrázkov

    • potom pre funkciu zapis() z úlohy (13)

      >>> zapis(citaj('tiger.bmp', 'image0.png', 'image1.png'), 'temp/obrazok', 'jpg')
      
    • vytvorí kópie 3 zadaných súborov s novými menami (v priečinku 'temp') vo formáte 'jpg'