12 - Grafika - `Pillow` i `Pycairo`
Grafika bitmapowa (pikselowa, rastrowa) z pillow
Pracę z grafiką bitmapową w Pythonie pokażę na przykładzie biblioteki pillow
.
Stronę domową, wraz dokumentacją można znaleźć pod adresem: https://pypi.org/project/Pillow/.
Zacznijmy od instalacji pillow
:
W środowisku Anaconda wykonaj polecenie:
conda install pillow
Zaimportujmy odpowiednie moduły, które wykorzystamy.
import os
from PIL import Image, ImageEnhance, ImageDraw, ImageFont, ImageFilter
Do kolejnych ćwiczeń użyjemy poniższej fotografii (choć możesz użyć własnej).
Pobierz ją i zapisz pod nazwą lokomotywa.jpg
w miejscu, w którym uruchamiasz kod w podkatalogu zdjecia
.
Napiszmy funkcję, która będzie wyświetlała plik a także drukowała niektóre informacje na temat pliku. Będziemy ją dalej wykorzystywać do podglądania efektów działań na zdjęciu.
def sprawdz_plik(sciezka, plik):
# Otwarcie pliku
foto = Image.open(os.path.join(sciezka, plik))
# Wyświetlenie obrazu
foto.show()
# Wydruk danych o pliku
print(f'Plik: {plik},\
format: {foto.format},\
rozmiar: {foto.size},\
tryb: {foto.mode}')
Użyjmy funkcji sprawdz_plik()
do wyświetlenia pliku i sprawdzenia jego parametrów:
# Ścieżka do katalogu ze zdjęciem
sciezka = 'zdjecia'
plik = 'lokomotywa.jpg'
# Wyświetlenie pliku i wydruk informacji
sprawdz_plik(sciezka, plik)
Plik: lokomotywa.jpg, format: JPEG, rozmiar: (700, 525), tryb: RGB
Teraz wykonamy kolejno serię manipulacji na zdjęciu, aby pokazać niektóre (!) możliwości pillow
.
Eksport z formatu JPG
do PNG
:
# Otwarcie pliku
foto = Image.open(os.path.join(sciezka, plik))
# Konwersja do formatu png
nazwa, przedluzenie = os.path.splitext(plik)
plik_png = nazwa + ".png"
foto.save(os.path.join(sciezka,plik_png))
sprawdz_plik(sciezka, plik_png)
Plik: lokomotywa.png, format: PNG, rozmiar: (700, 525), tryb: RGB
Tworzenie kopii o mniejszych rozmiarach:
# Tworzenie kopii o zmniejszonych wymiarach
# kopiujemy zdjęcie
foto_maly = foto.copy()
# zmiana rozmiaru kopii do maksymalnie 400x400 px
# proporcje są zachowane
foto_maly.thumbnail((400, 400))
plik_maly = nazwa+'_maly.jpg'
foto_maly.save(os.path.join(sciezka, plik_maly),'JPEG')
sprawdz_plik(sciezka, plik_maly)
Plik: lokomotywa_maly.jpg, format: JPEG, rozmiar: (400, 300), tryb: RGB
Obrót zdjęcia o 90, 180 i 270 stopni z użyciem metody transpose()
# Obracanie obrazka o 90 stopni
foto_90 = foto.transpose(Image.ROTATE_90)
foto_90.save(os.path.join(sciezka, nazwa + '_90.jpg'),'JPEG')
sprawdz_plik(sciezka, nazwa + '_90.jpg')
Plik: lokomotywa_90.jpg, format: JPEG, rozmiar: (525, 700), tryb: RGB
# Obracanie obrazka o 180 stopni
foto_180 = foto.transpose(Image.ROTATE_180)
foto_180.save(os.path.join(sciezka, nazwa + '_180.jpg'),'JPEG')
sprawdz_plik(sciezka, nazwa + '_180.jpg')
Plik: lokomotywa_180.jpg, format: JPEG, rozmiar: (700, 525), tryb: RGB
# Obracanie obrazka o 270 stopni
foto_270 = foto.transpose(Image.ROTATE_270)
foto_270.save(os.path.join(sciezka, nazwa + '_270.jpg'),'JPEG')
sprawdz_plik(sciezka, nazwa + '_270.jpg')
Plik: lokomotywa_270.jpg, format: JPEG, rozmiar: (525, 700), tryb: RGB
Obrót o dowolny kąt możemy osiągnąć używając metodę rotate()
# Obracanie obrazka o dowolny kąt
# (w kierunku przeciwnym do ruchu wskazówek zegara!)
foto_30 = foto.rotate(30)
foto_30.save(os.path.join(sciezka, nazwa + '_30.jpg'),'JPEG')
sprawdz_plik(sciezka, nazwa + '_30.jpg')
Plik: lokomotywa_30.jpg, format: JPEG, rozmiar: (700, 525), tryb: RGB
Łatwo możemy odwrócić zdjęcie w poziomie i w pionie:
# Odwracanie obrazka w poziomie
foto_LR = foto.transpose(Image.FLIP_LEFT_RIGHT)
foto_LR.save(os.path.join(sciezka, nazwa + '_LR.jpg'),'JPEG')
sprawdz_plik(sciezka, nazwa + '_LR.jpg')
Plik: lokomotywa_LR.jpg, format: JPEG, rozmiar: (700, 525), tryb: RGB
# Odwracanie obrazka w w pionie
foto_TB = foto.transpose(Image.FLIP_TOP_BOTTOM)
foto_TB.save(os.path.join(sciezka, nazwa + '_TB.jpg'),'JPEG')
sprawdz_plik(sciezka, nazwa + '_TB.jpg')
Plik: lokomotywa_TB.jpg, format: JPEG, rozmiar: (700, 525), tryb: RGB
Zmieńmy kontrast zdjęcia:
# Zmiana kontrastu - zwiększenie
foto_contr = ImageEnhance.Contrast(foto)
foto_contr.enhance(1.7).save(os.path.join(sciezka, nazwa + '_contrast_zwieksz.jpg'),'JPEG')
sprawdz_plik(sciezka, nazwa + '_contrast_zwieksz.jpg')
Plik: lokomotywa_contrast_zwieksz.jpg, format: JPEG, rozmiar: (700, 525), tryb: RGB
# Zmiana kontrastu - zmniejszenie
foto_contr = ImageEnhance.Contrast(foto)
foto_contr.enhance(0.2).save(os.path.join(sciezka, nazwa + '_contrast_zmniejsz.jpg'),'JPEG')
sprawdz_plik(sciezka, nazwa + '_contrast_zmniejsz.jpg')
Plik: lokomotywa_contrast_zmniejsz.jpg, format: JPEG, rozmiar: (700, 525), tryb: RGB
Możemy też pobrać wycinek zdjęcia:
# Wycinek zdjęcia
# Podajemy współrzędne prostokąta: lewy, górny, prawy, dolny
wycinek = (425, 250, 500, 350)
foto_wycinek = foto.crop(wycinek)
foto_wycinek.save(os.path.join(sciezka, nazwa + '_wycinek.jpg'),'JPEG')
sprawdz_plik(sciezka, nazwa + '_wycinek.jpg')
Plik: lokomotywa_wycinek.jpg, format: JPEG, rozmiar: (75, 100), tryb: RGB
Przekonwertujmy teraz zdjęcie na odcienie szarości:
# Zmiana na fotografię w odcieniach szarości;
foto_szare = foto.convert('L')
foto_szare.save(os.path.join(sciezka, nazwa + '_szare.jpg'),'JPEG')
sprawdz_plik(sciezka, nazwa + '_szare.jpg')
Plik: lokomotywa_szare.jpg, format: JPEG, rozmiar: (700, 525), tryb: L
Połączmy konwersję na odcienie szarości ze zwiększeniem kontrastu:
# Dodajemy kontrast
foto_szare_kontrast = ImageEnhance.Contrast(foto_szare)
foto_szare_kontrast.enhance(1.7).save(os.path.join(sciezka, nazwa + '_szare_contrast.jpg'),'JPEG')
sprawdz_plik(sciezka, nazwa + '_szare_contrast.jpg')
Plik: lokomotywa_szare_contrast.jpg, format: JPEG, rozmiar: (700, 525), tryb: L
Teraz rozjaśnimy a potem przyciemnimy zdjęcie:
# Rozjaśnienie zdjęcia
foto_jasne = ImageEnhance.Brightness(foto)
foto_jasne.enhance(1.75).save(os.path.join(sciezka, nazwa + '_jasne.jpg'),'JPEG')
sprawdz_plik(sciezka, nazwa + '_jasne.jpg')
Plik: lokomotywa_jasne.jpg, format: JPEG, rozmiar: (700, 525), tryb: RGB
# przyciemnienie zdjęcia
foto_ciemne = ImageEnhance.Brightness(foto)
foto_ciemne.enhance(0.50).save(os.path.join(sciezka, nazwa + '_ciemne.jpg'),'JPEG')
sprawdz_plik(sciezka, nazwa + '_ciemne.jpg')
Plik: lokomotywa_ciemne.jpg, format: JPEG, rozmiar: (700, 525), tryb: RGB
Możemy też wyostrzyć i rozmyć obrazek:
# Wyostrzenie zdjęcia
foto_wyostrzone = ImageEnhance.Sharpness(foto)
foto_wyostrzone.enhance(5).save(os.path.join(sciezka, nazwa + '_wyostrzone.jpg'),'JPEG')
sprawdz_plik(sciezka, nazwa + '_wyostrzone.jpg')
Plik: lokomotywa_wyostrzone.jpg, format: JPEG, rozmiar: (700, 525), tryb: RGB
# Rozmycie zdjęcia
foto_rozmyte = ImageEnhance.Sharpness(foto)
foto_rozmyte.enhance(0.01).save(os.path.join(sciezka, nazwa + '_rozmyte.jpg'),'JPEG')
sprawdz_plik(sciezka, nazwa + '_rozmyte.jpg')
Plik: lokomotywa_rozmyte.jpg, format: JPEG, rozmiar: (700, 525), tryb: RGB
Efektu rozmywania nie widać zbyt dobrze na wygenerowanym zdjęciu, lepszy efekt uzyskamy stosując odpowiedni filtr (jest ich dostępnych oczywiście więcej):
foto_rozmyte_gaussian = foto.filter(ImageFilter.GaussianBlur(5))
foto_rozmyte_gaussian.save(os.path.join(sciezka, nazwa + '_rozmyte_gaussian.jpg'),'JPEG')
sprawdz_plik(sciezka, nazwa + '_rozmyte_gaussian.jpg')
Plik: lokomotywa_rozmyte_gaussian.jpg, format: JPEG, rozmiar: (700, 525), tryb: RGB
Dodajmy do zdjęcia napis:
foto_kopia = foto.copy()
foto_napis = ImageDraw.Draw(foto_kopia)
# Font
font = ImageFont.truetype('arial.ttf', 40)
# fill oznacza kolor, można też określać go np. podając wartości RGB
foto_napis.text((10, 470),"Lokomotywa", font=font, fill='wheat')
foto_kopia.save(os.path.join(sciezka, nazwa + '_napis.jpg'),'JPEG')
sprawdz_plik(sciezka, nazwa + '_napis.jpg')
Plik: lokomotywa_napis.jpg, format: JPEG, rozmiar: (700, 525), tryb: RGB
Narysujmy kilka figur:
foto_kopia = foto.copy()
foto_rysuj = ImageDraw.Draw(foto_kopia)
# Rysujemy linię
foto_rysuj.line((10, 10, 100, 200), fill='yellow', width=4)
# Rysujemy prostokąt
foto_rysuj.rectangle((100, 100, 200, 200),
# Kolor wypełnienia
# Tym razem zastosujemy wartości RGB
fill=(255, 0, 0),
# kolor obrysu
outline=(0, 200, 0),
# grubość obrysu
width = 4)
# Rysujemy elipsę, która jest wypełnia prostokąt o podanych rozmiarach
foto_rysuj.ellipse((300, 300, 450, 400),
# Kolor wypełnienia
fill=(200, 0, 100),
# kolor obrysu
outline=(100, 100, 100),
# grubość obrysu
width = 10)
foto_kopia.save(os.path.join(sciezka, nazwa + '_figury.jpg'),'JPEG')
sprawdz_plik(sciezka, nazwa + '_figury.jpg')
Plik: lokomotywa_figury.jpg, format: JPEG, rozmiar: (700, 525), tryb: RGB
Zauważ, że układ współrzędnych używany powyżej (i w wielu innych innych przypadkach grafiki komputerowej) różni się od tego, którego używaliśmy w szkole.
Lewy, górny róg obrazu ma współrzędne (0, 0)
prawy dolny róg ma współrzędne wynikające z rozmiarów obrazu: (szerokosc, wysokosc)
.
Używając pętli możemy oczywiście zautomatyzować modyfikację wielu zdjęć.
Przykładowo, poniższy kod zmniejszy wielkość wszystkich zdjęć (o podanych przedłużeniach nazw plików) w katalogu do maksymalnego rozmiaru 100x100
pikseli i zapisuje je w formacie png
.
# Katalog ze zdjęciami
katalog_glowny = 'fotografie'
# Katalog, w kórym znajdą się miniaturki
katalog_wyjsciowy = os.path.join(katalog_glowny, 'miniaturki')
# Lista plików/katalogów w katalogu ze zdjęciami
obrazki = os.listdir(katalog_glowny)
print(f'Plik/katalogi: {obrazki}')
# Przedłużenia plików ze zdjęciami (można dopisać więcej)
przedluzenia = ['.jpg', '.jpeg', '.png']
# Utworzenie katalogu na miniaturki, jeśli nie istnieje
if os.path.exists(katalog_wyjsciowy):
print(f'katalog:{katalog_wyjsciowy} istnieje')
else:
print(f'Tworzę {katalog_wyjsciowy}')
os.makedirs(katalog_wyjsciowy)
#Przetwarzanie każdego zdjęcia
for plik in obrazki:
nazwa, przedluzenie = os.path.splitext(plik)
if (przedluzenie in przedluzenia):
foto = Image.open(os.path.join(katalog_glowny, plik))
nazwa, przedluzenie = os.path.splitext(plik)
plik_maly = nazwa + "-miniatura.png"
foto.thumbnail((100, 100))
foto.save(os.path.join(katalog_wyjsciowy, plik_maly),'PNG')
Plik/katalogi: ['lokomotywa.png', 'lokomotywa_szare_contrast.jpg', 'lokomotywa_rozmyte_gaussian.jpg', 'bbb.md', 'lokomotywa_szare.jpg', 'lokomotywa_maly.jpg', 'lokomotywa_TB.jpg', 'lokomotywa_contrast_zmniejsz.jpg', 'lokomotywa.jpg', 'lokomotywa_90.jpg', 'lokomotywa_figury.jpg', 'aaa.txt', 'lokomotywa_contrast_zwieksz.jpg']
Tworzę fotografie/miniaturki
Grafika wektorowa z użyciem modułu Pycairo
Do pracy z grafiką wektorową użyjemy modułu Pycairo
, którego dokumentację możemy znaleźć pod adresem: https://pycairo.readthedocs.io/en/latest/.
Pycairo
zainstalujemy poleceniem:
conda install pycairo
Narysujmy prosty rysunek i zapiszmy go w formacie PDF
.
Narysujemy prostokąt korzystając z metody rectangle()
, którego wywołanie wygląda tak:
rectangle(x, y, szerokosc, wysokosc)
Gdzie:
x
iy
to współrzędne lewego, górnego rogu.szerokosc
iwysokosc
określają rozmiar prostokąta.
Także w tym przypadku, lewy górny róg rysunku ma współrzędne (0, 0)
.
# Import modułu
import cairo
plik = 'rysunek_1.pdf'
szerokosc = 600
wysokosc = 400
# Tworzenie "surface" - obiektu w którym umieścimy rysunek
# przy tworzeniu określamy format, tu PDF
powierzchnia = cairo.PDFSurface(plik, szerokosc, wysokosc)
# Tworzenie "context" - na nim będziemy rysować
kontekst = cairo.Context(powierzchnia)
# Ustawiamy kolor (tła)
# Kolejne liczby określają wartości dla kanałów:
# R(ed), G(reen), B(lue) w zakresie 0-1
kontekst.set_source_rgb(0.8, 0.8, 0.9)
# Wypełniamy obszar rysunku wybranym kolorem
kontekst.paint()
# Rysujemy prostokąt
kontekst.rectangle(150, 100, 320, 240)
# Określamy kolor
kontekst.set_source_rgb(0, 0, 0.5)
# Wypełniamy figurę
kontekst.fill()
# Zapis pliku
powierzchnia.finish()
W powyższym przykładzie utworzyliśmy powierzchnię (surface
) w formacie PDF
.
W ten sposób określamy format wygenerowanego pliku.
Dostępnych jest kilka rodzajów powierzchni służących generowaniu plików w określonym formacie:
ImageSurface
- plik bitmapowy, w formaciePNG
PDFSurface
- plik wektorowy w formaciePDF
PSSsurface
- plik wektorowy w formaciePostScript
(PS
) lubEncapsulated PostScipt
(EPS
)SVGSurface
- plik wektorowy w formacieSVG
Są jeszcze inne dostępne powierzchnie, ale nie będziemy ich tu omawiać.
Wybór formatu generowanego pliku zależy oczywiście przede wszystkim od jego przeznaczenia.
Jeśli zamierzamy później ręcznie modyfikować wygenerowany obraz, warto wybrać któryś z formatów wektorowych.
Zwłaszcza formaty PDF
czy SVG
pozwalają na łatwą dalszą edycję w takich programach jak Inkscape.
Utwórzmy zatem podobny rysunek, tyle że w formacie SVG
.
plik = 'rysunek_1.svg'
szerokosc = 600
wysokosc = 400
# Tworzenie "surface" - obiektu w którym umieścimy rysunek
# przy tworzeniu określamy format, tu SVG
powierzchnia = cairo.SVGSurface(plik, szerokosc, wysokosc)
# Tworzenie "context" - na nim będziemy rysować
kontekst = cairo.Context(powierzchnia)
# Ustawiamy kolor (tła)
kontekst.set_source_rgb(0.8, 0.8, 0.9)
# Wypełniamy obszar rysunku wybranym kolorem
kontekst.paint()
# Rysujemy prostokąt
kontekst.rectangle(150, 100, 320, 240)
# Określamy kolor
kontekst.set_source_rgb(0, 0, 0.5)
# Wypełniamy figurę
kontekst.fill()
# Zapis pliku
powierzchnia.finish()
Nieco inaczej będzie to wyglądało w przypadku pliku w formacie PNG
.
Zwróć uwagę na nieco inny sposób tworzenia powierzchni i zapisu pliku.
plik = 'rysunek_1.png'
szerokosc = 600
wysokosc = 400
powierzchnia = cairo.ImageSurface(cairo.FORMAT_RGB24, szerokosc, wysokosc)
kontekst = cairo.Context(powierzchnia)
# Ustawiamy kolor (tła)
kontekst.set_source_rgb(0.8, 0.8, 0.9)
# Wypełniamy obszar rysunku wybranym kolorem
kontekst.paint()
# Rysujemy prostokąt
kontekst.rectangle(150, 100, 320, 240)
# Określamy kolor
kontekst.set_source_rgb(0, 0, 0.5)
# Wypełniamy figurę
kontekst.fill()
# Zapis pliku
powierzchnia.write_to_png(plik)
Dalej będziemy generowali pliki w formacie PNG
aby łatwiej podejrzeć wynik.
Linie proste, łamane i wielokąty
Zacznijmy od linii prostych, łamanych i wielokątów. Aby je narysować wykorzystamy metody move_to()
oraz line_to()
:
plik = 'rysunek_2.png'
szerokosc = 400
wysokosc = 300
powierzchnia = cairo.ImageSurface(cairo.FORMAT_RGB24, szerokosc, wysokosc)
kontekst = cairo.Context(powierzchnia)
# Ustawiamy kolor (tła) na jasnoszary
kontekst.set_source_rgb(0.95, 0.95, 0.95)
# Wypełniamy obszar rysunku wybranym kolorem
kontekst.paint()
# Zmieniamy punkt początkowy na (10, 20)
kontekst.move_to(10, 20)
# Tworzymy linię do punktu (200, 200)
kontekst.line_to(200, 200)
# Ustawiamy kolor na czarny
kontekst.set_source_rgb(0, 0, 0)
# Szerokość linii
kontekst.set_line_width(10)
# Teraz rysujemy linię
kontekst.stroke()
# Zapis pliku
powierzchnia.write_to_png(plik)
Po wykonaniu metody line_to()
zmienia się także punkt początkowy, od którego możemy tworzyć np. następną linię.
Umożliwia to rysowanie krzywych:
plik = 'rysunek_3.png'
szerokosc = 400
wysokosc = 300
powierzchnia = cairo.ImageSurface(cairo.FORMAT_RGB24, szerokosc, wysokosc)
kontekst = cairo.Context(powierzchnia)
# Ustawiamy kolor (tła) na jasnoszary
kontekst.set_source_rgb(0.95, 0.95, 0.95)
# Wypełniamy obszar rysunku wybranym kolorem
kontekst.paint()
# Zmieniamy punkt początkowy na (10, 20)
kontekst.move_to(10, 20)
# Tworzymy linię do punktu (200, 300)
kontekst.line_to(200, 200)
# Kolejne linie:
kontekst.line_to(100, 200)
kontekst.line_to(300, 20)
kontekst.line_to(300, 290)
kontekst.line_to(390, 290)
# Ustawiamy kolor na czarny
kontekst.set_source_rgb(0, 0, 0)
# Szerokość linii
kontekst.set_line_width(10)
# Teraz rysujemy linię
kontekst.stroke()
# Zapis pliku
powierzchnia.write_to_png(plik)
Można też utworzyć zamknięte wielokąty a także je wypełnić:
plik = 'rysunek_4.png'
szerokosc = 400
wysokosc = 300
powierzchnia = cairo.ImageSurface(cairo.FORMAT_RGB24, szerokosc, wysokosc)
kontekst = cairo.Context(powierzchnia)
# Ustawiamy kolor (tła) na jasnoszary
kontekst.set_source_rgb(0.95, 0.95, 0.95)
# Wypełniamy obszar rysunku wybranym kolorem
kontekst.paint()
# Zmieniamy punkt początkowy na (100, 200)
kontekst.move_to(100, 100)
# Tworzymy kolejne krawędzie wielokąta
kontekst.line_to(200, 250)
kontekst.line_to(380, 150)
kontekst.line_to(250, 80)
kontekst.line_to(250, 10)
kontekst.line_to(200, 10)
# Zamykamy figurę
kontekst.close_path()
# Kolor wypełnienia
kontekst.set_source_rgb(0.2, 0, 0.8)
# Wypełniamy figurę, ale zachowujemy ścieżkę
kontekst.fill_preserve()
# Ustawiamy kolor na czarny
kontekst.set_source_rgb(0, 0, 0)
# Szerokość linii
kontekst.set_line_width(10)
# Rysujemy obrys
kontekst.stroke()
# Zapis pliku
powierzchnia.write_to_png(plik)
Łuki i okręgi
Łuki i okręgi możemy rysować przy pomocy metody arc()
.
import math
plik = 'rysunek_5.png'
szerokosc = 400
wysokosc = 300
powierzchnia = cairo.ImageSurface(cairo.FORMAT_RGB24, szerokosc, wysokosc)
kontekst = cairo.Context(powierzchnia)
# Ustawiamy kolor (tła) na jasnoszary
kontekst.set_source_rgb(0.95, 0.95, 0.95)
# Wypełniamy obszar rysunku wybranym kolorem
kontekst.paint()
# Pierwszy łuk (okrąg)
kontekst.arc(200, 150, 120, 0, math.pi*2)
kontekst.set_source_rgb(0.5, 0.5, 1)
kontekst.set_line_width(10)
kontekst.stroke()
# Drugi łuk
kontekst.arc(200, 150, 80, math.pi/2, math.pi)
kontekst.set_source_rgb(1, 0.7, 0)
kontekst.set_line_width(10)
kontekst.stroke()
# Trzeci łuk
kontekst.arc(200, 150, 50, 0, math.pi)
kontekst.set_source_rgb(0.5, 0.9, 0.5)
kontekst.set_line_width(10)
kontekst.stroke()
# Zapis pliku
powierzchnia.write_to_png(plik)
Rysowanie łuków i okręgów wymaga pewnego wyjaśnienia.
Przyjrzyj się poniższemu schematowi:
Wywołanie metody wygląda następująco:
arc(x, y, promień, kąt1, kąt2)
Współrzędne x
i y
wyznaczają punkt, który jest środkiem okręgu, o promieniu promień
, z którego ,,wycinany’’ jest łuk.
Wyobraźmy sobie linię, która przebiega poziomo na wysokości wyznaczonej przez y
(linia0
).
Zgodnie z ruchem zegara wyznaczany jest kąt1
, który określa początek rysowania łuku, który sięga miejsca gdzie kończy się łuk2
, który także bierze początek na linii0
.
Co ważne, kąty podajemy nie w stopniach, ale w radianach. 180° odpowiada wartości π
(3,14…), zatem np. 90° będzie odpowiadało π/2
, a dla pełnego okręgu (360°) będzie to 2*π
.
Radiany na stopnie możemy łatwo przeliczać wykorzystując moduł math
:
# Stopnie vs. radiany.
import math
print (f'180 stopni to {math.radians(180)} radianów')
print (f'90 stopni to {math.radians(90)} radianów')
print (f'30 stopni to {math.radians(30)} radianów')
print (f'Pi radianów to {math.degrees(math.pi)} stopni')
print (f'2 Pi radianów to {math.degrees(2*math.pi)} stopni')
180 stopni to 3.141592653589793 radianów
90 stopni to 1.5707963267948966 radianów
30 stopni to 0.5235987755982988 radianów
Pi radianów to 180.0 stopni
2 Pi radianów to 360.0 stopni
Można też użyć innego sposobu:
import math
for st in [180, 90, 30]:
print (f'{st}*math.pi/180 to: {st*math.pi/180} radianów ({st} stopni)')
180*math.pi/180 to: 3.141592653589793 radianów (180 stopni)
90*math.pi/180 to: 1.5707963267948966 radianów (90 stopni)
30*math.pi/180 to: 0.5235987755982988 radianów (30 stopni)
Spróbujmy teraz zastosować wypełnienie dla uzyskanych łuków (i okręgu).
import math
plik = 'rysunek_6.png'
szerokosc = 400
wysokosc = 300
powierzchnia = cairo.ImageSurface(cairo.FORMAT_RGB24, szerokosc, wysokosc)
kontekst = cairo.Context(powierzchnia)
# Ustawiamy kolor (tła) na jasnoszary
kontekst.set_source_rgb(0.95, 0.95, 0.95)
# Wypełniamy obszar rysunku wybranym kolorem
kontekst.paint()
# Pierwszy łuk (okrąg)
kontekst.arc(200, 150, 120, 0, math.pi*2)
kontekst.set_source_rgb(0.5, 0.5, 1)
kontekst.set_line_width(10)
kontekst.fill()
kontekst.stroke()
# Drugi łuk
kontekst.arc(200, 150, 80, math.pi/2, math.pi)
kontekst.set_source_rgb(1, 0.7, 0)
kontekst.set_line_width(10)
kontekst.fill()
kontekst.stroke()
# Trzeci łuk z krawędzią
kontekst.arc(200, 150, 50, 0, math.pi)
kontekst.set_line_width(10)
kontekst.set_source_rgb(0.2, 0.2, 0.2)
# stroke_preserve() aby łuk był widoczny
kontekst.stroke_preserve()
kontekst.set_source_rgb(0.5, 0.9, 0.5)
kontekst.fill()
# Zapis pliku
powierzchnia.write_to_png(plik)
Tekst
Na tworzonym rysunku, można też umieścić tekst. Poniższy kod pokazuje kilka przykładów pozwalających zrozumieć podstawowe zasady dotyczące umieszczenia i modyfikowania wyglądu tekstu. Zwróć zwłaszcza uwagę na umieszczenie tekstu względem zielonej linii. Jej początek, a także miejsce wstawienia pierwszego tekstu, to ten sam punkt.
plik = 'rysunek_7.png'
szerokosc = 600
wysokosc = 400
powierzchnia = cairo.ImageSurface(cairo.FORMAT_RGB24, szerokosc, wysokosc)
kontekst = cairo.Context(powierzchnia)
# Ustawiamy kolor (tła) na jasnoszary
kontekst.set_source_rgb(0.95, 0.95, 0.95)
# Wypełniamy obszar rysunku wybranym kolorem
kontekst.paint()
# Współrzędne punktów
x1, y1 = 50, 50
x2, y2 = 550, 50
# Linia zielona ("referencyjna")
kontekst.move_to(x1, y1)
kontekst.line_to(x2, y2)
kontekst.set_source_rgb(0, 1, 0)
kontekst.set_line_width(2)
kontekst.stroke()
# Definiujemy font i jego parametry
kontekst.select_font_face("Serif")
# Miejsce umieszczenie tekstu
kontekst.move_to(x1, y1)
# Kolor tekstu
kontekst.set_source_rgb(0.7, 0, 0)
# Rozmiar fontu
kontekst.set_font_size(30)
# Umieszczenie tekstu
kontekst.show_text("Wprowadzony tekst")
# Różne style fontu
kontekst.move_to(x1, y1 + 50)
kontekst.select_font_face('Serif', cairo.FontSlant.ITALIC)
kontekst.show_text("Wprowadzony tekst, italik")
kontekst.move_to(x1, y1 + 100)
kontekst.select_font_face('Serif', cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
kontekst.show_text("Wprowadzony tekst, bold")
kontekst.move_to(x1, y1 + 150)
kontekst.select_font_face('Serif', cairo.FontSlant.ITALIC, cairo.FontWeight.BOLD)
kontekst.show_text("Wprowadzony tekst, italik, bold")
# Inne kroje fontu
kontekst.move_to(x1, y1 + 200)
kontekst.select_font_face('Arial')
kontekst.show_text("Wprowadzony tekst, Arial")
kontekst.move_to(x1, y1 + 250)
kontekst.select_font_face('Times')
kontekst.show_text("Wprowadzony tekst, Times")
kontekst.move_to(x1, y1 + 300)
kontekst.select_font_face('Courier')
kontekst.show_text("Wprowadzony tekst, Courier")
powierzchnia.write_to_png(plik)
Krzywe Béziera
Krzywa Béziera pozwalają na tworzenie krzywych, nawet o dość złożonych kształtach. Nie będę tu głębiej omawiał terorii, moża o niej przeczytać np. na stronie Wikipedii https://pl.wikipedia.org/wiki/Krzywa_B%C3%A9ziera. W praktyce, aby ,,wyczuć’’ ten sposób rysowania krzywych, warto poćwiczyć w programach przeznaczonych do tworzenia grafiki wektorowej, takich jak darmowy Inkscape.
Poniżej znajduje się kod, który generuje obrazek, pozwalający nieco wyjaśnić jak powstają takie krzywe.
Wyobraźmy sobie, że mamy prostą (zielona) przebiegającą od punktu A
i B
.
Następnie definiujemy punkty P1
i P2
, które kontrolują kształt powstającej krzywej, co możemy sobie wyobrazić jako ,,przyciąganie’’ krzywej.
Poniższy przykład tworzy dość prostą krzywą, możliwe jest tworzenie dużo bardziej skomplikowanych kształtów, czym jednak nie będziemy się tu zajmować.
plik = 'rysunek_8.png'
szerokosc = 450
wysokosc = 300
powierzchnia = cairo.ImageSurface(cairo.FORMAT_RGB24, szerokosc, wysokosc)
kontekst = cairo.Context(powierzchnia)
# Ustawiamy kolor (tła) na jasnoszary
kontekst.set_source_rgb(0.95, 0.95, 0.95)
# Wypełniamy obszar rysunku wybranym kolorem
kontekst.paint()
# Współrzędne punktów
x1, y1 = 50, 150
x2, y2 = 350, 150
px1, py1 = 100, 250
px2, py2 = 290, 50
# Linia zielona ("referencyjna")
kontekst.move_to(x1, y1)
kontekst.line_to(x2, y2)
kontekst.set_source_rgb(0, 1, 0)
kontekst.set_line_width(7)
kontekst.stroke()
# Krzywe Béziera (pomarańczowa)
kontekst.move_to(x1, y1)
kontekst.curve_to(px1, py1, px2, py2, x2, y2)
kontekst.set_source_rgb(1, 0.7, 0)
kontekst.set_line_width(10)
kontekst.stroke()
# Linie pomiędzy punktami A-P1, oraz B-P2
kontekst.move_to(x1, y1)
kontekst.line_to(px1, py1)
kontekst.move_to(x2, y2)
kontekst.line_to(px2, py2)
kontekst.set_source_rgb(0.7, 0.7, 1)
kontekst.set_line_width(2)
kontekst.stroke()
# Opisy
kontekst.select_font_face("Arial")
kontekst.set_font_size(16)
kontekst.set_source_rgb(0, 0, 0)
kontekst.move_to(x1, y1)
kontekst.show_text("A (x1, y1)")
kontekst.move_to(x2, y2)
kontekst.show_text("B (x2, y2)")
kontekst.move_to(px1, py1)
kontekst.show_text("P1 (px1, py1)")
kontekst.move_to(px2, py2)
kontekst.show_text("P2 (px2, py2)")
# Zapis pliku
powierzchnia.write_to_png(plik)
Obrys i wypełnienie figur
Zajmijmy się teraz modyfikacjami stylów linii, obrysów a także wypełnień rysowanych kształtów.
plik = 'rysunek_9.png'
szerokosc = 350
wysokosc = 350
powierzchnia = cairo.ImageSurface(cairo.FORMAT_RGB24, szerokosc, wysokosc)
kontekst = cairo.Context(powierzchnia)
kontekst.set_source_rgb(1, 1, 1)
kontekst.paint()
x, y = 120, 120
r = 100
# Pierwsze koło - czerwony, nieprzeźroczysty
kontekst.arc(x, y, r, 0, math.pi*2)
# Szerokość obrysu
kontekst.set_line_width(10)
# Kolor wypełnienia: czerwony, nieprzeźroczysty
kontekst.set_source_rgb(1, 0, 0)
# Wypełnienie, ale wciąż pracujemy nad tą samą figurą
kontekst.fill_preserve()
# Kolor obrysu, nieprzeźroczysty
kontekst.set_source_rgb(0, 0, 0)
kontekst.stroke()
# Drugie koło - zielone, półprzeźroczyste
kontekst.arc(x + r, y, r, 0, math.pi*2)
kontekst.set_line_width(10)
# Kolor wypełnienia: zielony, półprzeźroczysty
kontekst.set_source_rgba(0, 1, 0, 0.5)
kontekst.fill_preserve()
# Kolor obrysu, półprzeźroczysty
kontekst.set_source_rgba(0, 0, 0, 0.5)
kontekst.stroke()
# Trzecie koło - gradient, częściowo przeźroczysty, bez obrysu
kontekst.arc(x + r/2, y + r, r, 0, math.pi*2)
pattern = cairo.LinearGradient(x + r/2, y, x + r/2, y + 2*r)
# Pierwszy kolor
pattern.add_color_stop_rgb(0, 1, 1, 0)
# Drugi kolor - półprzeźroczysty
pattern.add_color_stop_rgba(0.5, 1, 0, 0, 0.5)
# Trzeci kolor
pattern.add_color_stop_rgb(1, 0, 0, 1)
kontekst.set_source(pattern)
kontekst.fill()
# Linia gradientu: biała - pokazuje przebieg gradientu
kontekst.move_to(x + r/2, y)
kontekst.line_to(x + r/2, y + 2*r)
kontekst.set_source_rgb(1, 1, 1)
kontekst.set_line_width(3)
kontekst.stroke()
powierzchnia.write_to_png(plik)
Większość kodu, zwłaszcza uwzględniając komentarze, powinna być zrozumiała. Zwrócę uwagę na kilka aspektów: Zauważ, że dla drugiego koła wykorzystaliśmy częściowo przeźroczyste wypełnienie i obrys:
kontekst.set_source_rgba(R, G, B, A)
Jak widać, poza standardowymi kanałami R(ed)
, G(reen)
, B(lue
), doszedł czwarty (A)lpha
pozwalający na ustawienie stopnia krycia, zatem możemy w ten sposób sprawić, że rysowane przez nas kształty są w różnym stopniu przeźroczyste.
Gradient liniowy (są też inne) określamy definiując przebieg linii gradientu (zaznaczony białą linią) wzdłuż, której zmieniają się kolory. Możemy ustawić kilka kolorów w określonych miejscach, pomiędzy którymi kolor przechodzi płynnie z jednego w drugi. Także one mogą być w różnym stopniu przeźroczyste.
Same linie, a także obrysy kształtów mogą mieć różne style a także rodzaje zakończeń. Zacznijmy od zakończeń. Wygeneruj rysunek za pomocą poniższego kodu i przeanalizuj wynik. Dwie pionowe, pomarańczowe linie wskazują na początek i koniec “właściwych linii’’ (czarnych), których położenie jest wyznaczone początkowymi i końcowymi punktami.
plik = 'rysunek_10.png'
szerokosc = 300
wysokosc = 250
powierzchnia = cairo.ImageSurface(cairo.FORMAT_RGB24, szerokosc, wysokosc)
kontekst = cairo.Context(powierzchnia)
kontekst.set_source_rgb(0.9, 0.9, 0.9)
kontekst.paint()
x1, y1 = 50, 20
x2, y2 = 250, 20
# Odstęp między liniami
odstep = 50
# Linie referencyjne (pomarańczowe) - pokazują,
# gdzie się zaczynają i kończą linie "właściwe"
grubosc = 4
kontekst.set_line_width(grubosc)
kontekst.set_source_rgb(1, 0.7, 0)
kontekst.move_to(x1, y1 - 10)
kontekst.line_to(x1, y2 + odstep * 4 + 10)
kontekst.stroke()
kontekst.move_to(x2, y1 - 10)
kontekst.line_to(x2, y2 + odstep * 4 + 10)
kontekst.stroke()
# "Właściwe" linie
grubosc = 20
kontekst.set_line_width(grubosc)
kontekst.set_source_rgb(0, 0, 0)
# Linia
kontekst.move_to(x1, y1)
kontekst.line_to(x2, y2)
kontekst.stroke()
# Linia z zakończeniem zaokrąglonym
kontekst.set_line_cap(cairo.LINE_CAP_ROUND)
kontekst.move_to(x1, y1 + odstep)
kontekst.line_to(x2, y2+ odstep)
kontekst.stroke()
# Linia z zakończeniem kwadratowym
kontekst.set_line_cap(cairo.LINE_CAP_SQUARE)
kontekst.move_to(x1, y1 + odstep * 2)
kontekst.line_to(x2, y2 + odstep * 2)
kontekst.stroke()
# Brak dodatkowego zakończenia (domyślnie)
kontekst.set_line_cap(cairo.LINE_CAP_BUTT)
kontekst.move_to(x1, y1 + odstep * 3)
kontekst.line_to(x2, y2 + odstep * 3)
kontekst.stroke()
# Zaokrąglone zakończenie, dopasowane długością
# do linii bez zakończenia
kontekst.set_line_cap(cairo.LINE_CAP_ROUND)
kontekst.move_to(x1 + grubosc/2, y1 + odstep * 4)
kontekst.line_to(x2 - grubosc/2, y2 + odstep * 4)
kontekst.stroke()
powierzchnia.write_to_png(plik)
Zauważ, że po dodaniu zakończeń, linia wraz zakończeniem, staje się dłuższa. Jeśli chcemy utrzymać długość taką, jak przed dodaniem zakończeń, należy odpowiednio skrócić linie, co widać w piątej linii.
Teraz przyjrzyjmy się jak można zmienić styl linii:
def rysuj_linie(linia):
kontekst.move_to(x1, y1 + odstep * linia)
kontekst.line_to(x2, y2 + odstep * linia)
kontekst.stroke()
plik = 'rysunek_11.png'
szerokosc = 300
wysokosc = 250
powierzchnia = cairo.ImageSurface(cairo.FORMAT_RGB24, szerokosc, wysokosc)
kontekst = cairo.Context(powierzchnia)
kontekst.set_source_rgb(0.9, 0.9, 0.9)
kontekst.paint()
x1, y1 = 50, 20
x2, y2 = 250, 20
# Odstęp między liniami
odstep = 40
grubosc = 10
kontekst.set_line_width(grubosc)
kontekst.set_source_rgb(0, 0, 0)
# Linia
rysuj_linie(0)
# Linia złożona z odcinków widocznych i niewidocznych
# o tej samej długości, równej grubości linii
kontekst.set_dash([grubosc])
rysuj_linie(1)
# Można oczywiście podać konkretną liczbę
# określającą długość odcinków widocznych i niewidocznych
kontekst.set_dash([30])
rysuj_linie(2)
# Cześć niewidoczna linii 2 razy dłuższa niż widocznej
rysuj_linie(3)
# Część widoczna 2 * dłuższa niż grubość, niewidoczna = grubość
# widoczna = grubość, niewidoczna = grubość
kontekst.set_dash([grubosc * 2, grubosc, grubosc, grubosc])
rysuj_linie(4)
# Na zmianę części widoczne i niewidoczne, o długości:
# grubość, grubość, grubość * 2
kontekst.set_dash([grubosc, grubosc, grubosc * 2])
rysuj_linie(5)
powierzchnia.write_to_png(plik)
Teraz najpierw narysujemy takie same linie jak powyżej, ale z dodanymi zaokrąglonymi końcówkami i poniżej nich kilka przykładowych linii w stylach złożonych z kropek. Przeanalizuj kod.
def rysuj_linie(linia):
kontekst.move_to(x1, y1 + odstep * linia)
kontekst.line_to(x2, y2 + odstep * linia)
kontekst.stroke()
plik = 'rysunek_12.png'
szerokosc = 300
wysokosc = 250
powierzchnia = cairo.ImageSurface(cairo.FORMAT_RGB24, szerokosc, wysokosc)
kontekst = cairo.Context(powierzchnia)
kontekst.set_source_rgb(0.9, 0.9, 0.9)
kontekst.paint()
x1, y1 = 50, 20
x2, y2 = 250, 20
# Odstęp między liniami
odstep = 20
grubosc = 10
kontekst.set_line_width(grubosc)
kontekst.set_source_rgb(0, 0, 0)
# Ustawiamy zaokrąglone zakończenia dla linii i ich odcinków
kontekst.set_line_cap(cairo.LINE_CAP_ROUND)
# Linia
rysuj_linie(0)
# Linia złożona z odcinków widocznych i niewidocznych
# o tej samej długości, równej grubości linii
kontekst.set_dash([grubosc])
rysuj_linie(1)
# Można oczywiście podać konkretną liczbę
# określającą długość odcinków widocznych i niewidocznych
kontekst.set_dash([30])
rysuj_linie(2)
# Cześć niewidoczna linii 2 razy dłuższa niż widocznej
kontekst.set_dash([grubosc])
rysuj_linie(3)
# Część widoczna 2 * dłuższa niż grubość, niewidoczna = grubość
# widoczna = grubość, niewidoczna = grubość
kontekst.set_dash([grubosc * 2, grubosc, grubosc, grubosc])
rysuj_linie(4)
# Na zmianę części widoczne i niewidoczne, o długości:
# grubość, grubość, grubość * 2
kontekst.set_dash([grubosc, grubosc, grubosc * 2])
rysuj_linie(5)
# Długość = 0 (+zakończenia), grubość
kontekst.set_dash([0, grubosc])
rysuj_linie(7)
# Długość = 0 (+zakończenia), grubość
kontekst.set_dash([0, grubosc * 2])
rysuj_linie(8)
# Długość = 0 (+zakończenia), grubość
# Inna grubość linii
grubosc = 5
kontekst.set_line_width(grubosc)
kontekst.set_dash([0, grubosc * 2])
rysuj_linie(9)
# Dwie powyższe linie, narysowane w tym samym miejscu,
# jedna ze zmienionym kolorem
grubosc = 10
kontekst.set_line_width(grubosc)
kontekst.set_dash([0, grubosc * 2])
rysuj_linie(10)
kontekst.set_source_rgb(0.9, 0.3, 0.3)
grubosc = 5
kontekst.set_line_width(grubosc)
kontekst.set_dash([0, grubosc * 2])
rysuj_linie(10)
powierzchnia.write_to_png(plik)
Styl linii można zastosować także do obrysów figur:
plik = 'rysunek_13.png'
szerokosc = 600
wysokosc = 400
powierzchnia = cairo.ImageSurface(cairo.FORMAT_RGB24, szerokosc, wysokosc)
kontekst = cairo.Context(powierzchnia)
kontekst.set_source_rgb(0.8, 0.8, 0.8)
kontekst.paint()
kontekst.rectangle(szerokosc/4,
wysokosc/4,
szerokosc - szerokosc/4*2,
wysokosc - wysokosc/4*2)
kontekst.set_source_rgb(1, 0.7, 0)
kontekst.fill_preserve()
# Obrys
kontekst.set_line_cap(cairo.LINE_CAP_ROUND)
kontekst.set_source_rgb(0, 0, 0)
grubosc = 10
kontekst.set_line_width(grubosc)
kontekst.set_dash([0, grubosc * 2])
kontekst.stroke()
powierzchnia.write_to_png(plik)
Struktury fraktalopodobne, rekurencja i animacja
Komputerowe generowanie grafiki pozwala w stosunkowo łatwy sposób stworzyć złożone obrazy w oparciu o dość proste reguły.
Utwórzmy strukturę fraktalopodobną złożoną z kółek.
Przy okazji poznamy zastosowanie rekurencji.
Polega ona na tym, że dana funkcja odwołuje się do siebie samej.
Aby zabezpieczyć program przed zapętleniem należy umieścić w kodzie jakieś zabezpieczenie, które przerwie ten proces.
W poniższym przypadku mamy funkcję rysuj_kolo()
, która przyjmuje trzy argumenty:
r
- promień kołax
,y
- współrzędne położenia koła
Po wywołaniu funkcji, rysuje ona koło i jeśli r > 5
, to dwukrotnie wywołuje siebie samą ale zmniejszając promień koła o połowę oraz przesuwając położenie koła (o długość r
) w prawo, przy pierwszym wywołaniu, i w lewo przy drugim wywołaniu.
plik = 'fraktal.png'
szerokosc = 800
wysokosc = 400
powierzchnia = cairo.ImageSurface(cairo.FORMAT_RGB24, szerokosc, wysokosc)
kontekst = cairo.Context(powierzchnia)
kontekst.set_source_rgb(1, 1, 1)
kontekst.paint()
def rysuj_kolo(r, x, y):
kontekst.arc(x, y, r, 0, math.pi*2)
kontekst.set_line_width(1)
kontekst.set_source_rgba(0, 0.4, 0, 0.3)
kontekst.fill_preserve()
kontekst.set_source_rgba(0, 0.4, 0, 0.5)
kontekst.stroke()
if r > 5:
rysuj_kolo(r / 2, x + r, y)
rysuj_kolo(r / 2, x - r, y)
x, y = 400, 200
r = 180
rysuj_kolo(r, x, y)
powierzchnia.write_to_png(plik)
W zrozumieniu jak działa rekurencja w powyższym kodzie, pomoże zrozumieć animacja pokazująca kolejne etapy tworzenia struktury fraktalopodobnej.
Połączymy siły Pycairo
i pillow
aby wygenerować animację w formacie gif.
Nieco zmodyfikujemy powyższy kod generujący obrazek, tak aby zapisywał osobny plik dla każdego narysowanego koła.
Nazwy kolejnych wygenerowanych plików będziemy zapisywać w liście obrazki
.
Następnie wczytamy kolejno pliki z użyciem pillow
i dodamy je do kolejnej listy klatki
, której użyjemy w funkcji save
, która wygeneruje tym razem animowany gif
:
from PIL import Image, ImageDraw
import cairo
szerokosc = 800
wysokosc = 400
powierzchnia = cairo.ImageSurface(cairo.FORMAT_RGB24, szerokosc, wysokosc)
kontekst = cairo.Context(powierzchnia)
kontekst.set_source_rgb(1, 1, 1)
kontekst.paint()
obrazki = []
# Katalog na klatki - obrazki z kolejnymi etapami
# tworzenia obrazu
katalog_klatek='klatki'
# Utworzenie katalogu na klatki, jeśli nie istnieje
if os.path.exists(katalog_klatek):
print(f'katalog:{katalog_klatek} istnieje')
else:
print(f'Tworzę {katalog_klatek}')
os.makedirs(katalog_klatek)
def rysuj_kolo(r, x, y, n):
plik = f'{katalog_klatek}/fraktal_{n}.png'
obrazki.append(plik)
n += 1
kontekst.arc(x, y, r, 0, math.pi*2)
kontekst.set_line_width(1)
kontekst.set_source_rgba(0, 0.4, 0, 0.3)
kontekst.fill_preserve()
kontekst.set_source_rgba(0, 0.4, 0, 0.5)
kontekst.stroke()
powierzchnia.write_to_png(plik)
if r > 5:
n = rysuj_kolo(r / 2, x + r, y, n)
n = rysuj_kolo(r / 2, x - r, y, n)
return n
n = 1
x, y = 400, 200
r = 180
rysuj_kolo(r, x, y, n)
klatki = []
for plik in obrazki:
obrazek = Image.open(plik)
klatki.append(obrazek)
klatki[0].save('fraktal.gif',
save_all=True, append_images=klatki[1:], duration=100, loop=0)
print("Koniec")
Koniec