Iterowanie dwuwymiarowe: obróbka obrazów

Dwuwymiarowe tablice posiadają zarówno rzędy, jak i kolumny. Prawdopodobnie widzieliście wiele takich tablic, jeżeli używaliście kiedyś arkusza kalkulacyjnego. Innym obiektem zorganizowanym w kolumny i rzędy jest obraz cyfrowy. W tym rozdziale dowiemy się jak iterowanie może nam pomóc w manipulowaniu takimi obrazami.

Obraz cyfrowy to skończony zbiór małych elementów obrazu, zwanych pikselami. Piksele te poukładane są na dwuwymiarowej siatce. Każdy piksel reprezentuje najmniejszą ilość informacji dostępną dla danego obrazu. Czasami takie piksele wyglądają jak maleńkie „kropki”.

Każdy obraz (siatka pikseli) posiada zadaną szerokość i wysokość. Szerokość to po prostu liczba kolumn, a wysokość odpowiada liczbie rzędów. Możemy nazwać każdy piksel, używając numeru kolumny oraz rzędu. Trzeba pamiętać jednak, że informatycy lubią zaczynać liczyć od zera! Oznacza to, że jeżeli mamy 20 rzędów, będą one ponumerowane 0, 1, 2, aż do 19. Okaże się to bardzo użyteczne za chwilę, gdy będziemy iterować, używając funkcji range.

Na poniższym rysunku, piksel, który nas interesuje znajduje się w kolumnie c i rzędzie r.

../_images/image.png

Model RGB

Każdy piksel będzie reprezentował oddzielny kolor. Każdy z kolorów będzie zależny od wartości zwracanej z wzoru, którym wymieszamy różne ilości kolorów podstawowych: czerwonego (Red), zielonego (Green) oraz niebieskiego (Blue). Taka technika uzyskiwania palety kolorów znana jest jako Model RGB. Ilość danego koloru podstawowego, czasem nazywana natężeniem koloru, pozwoli nam dokładnie sterować wynikowym kolorem.

Minimalna wartość natężenia danego koloru podstawowego to 0. Jeżeli intensywność koloru czerwonego ustawiona jest na 0 oznacza to, że dany piksel nie posiada koloru czerwonego. Maksymalna intensywność to 255. Oznacza to, że w rzeczywistości mamy 256 stopni intensywności dla każdego z trzech kolorów podstawowych. Oznacza to, że możemy stworzyć 2563 różnych kolorów w Modelu RGB.

Poniżej znajdziecie natężenia czerwonego, zielonego i niebieskiego koloru dla kilku pospolitych kolorów. Proszę zauważyć, że kolor czarny jest reprezentowany przez piksel bez przypisanego koloru podstawowego, natomiast dla koloru białego wartości wszystkich trzech komponentów podstawowych są maksymalne (255).

Color Red Green Blue
Red 255 0 0
Green 0 255 0
Blue 0 0 255
White 255 255 255
Black 0 0 0
Yellow 255 255 0
Magenta 255 0 255

Aby móc manipulować obrazkiem, musimy mieć możliwość dostępu do indywidualnych pikseli. Taką możliwość udostępnia nam moduł o nazwie image. Zdefiniowane są w nim dwie klasy Image oraz Pixel.

Każdy obiekt typu Pixel posiada trzy atrybuty: natężenia koloru czerwonego, zielonego i niebieskiego. Klasa ta posiada trzy metody uzyskiwania informacji o wartościach natężenia poszczególnych kolorów. Są to: getRed (pobież informację o kolorze czerwonym), getGreen (zielonym) i getBlue (niebieskim). Dodatkowo możemy nakazać danemu pikselowi zmianę owej wartości natężenia, używając metod setRed (ustaw natężenie koloru czerwonego), setGreen i setBlue.

Nazwa metody Przykład Wytłumaczenie
Pixel(r,g,b) Pixel(20,100,50) Tworzy nowy piksel o natężeniach 20 czerwony, 100 zielony i 50 niebieski.
getRed() r = p.getRed() Zwraca natężenie składnika czerwonego.
getGreen() r = p.getGreen() Zwraca natężenie składnika zielonego.
getBlue() r = p.getBlue() Zwraca natężenie składnika niebieskiego.
setRed() p.setRed(100) Ustawia natężenie składnika czerwonego na 100.
setGreen() p.setGreen(45) Ustawia natężenie składnika zielonego na 100.
setBlue() p.setBlue(156) Ustawia natężenie składnika niebieskiego na 100.

W poniższym przykładzie, najpierw stworzymy piksel o natężeniach 45 dla czerwonego komponentu, 76 zielonego i 200 dla niebieskiego. Następnie wydrukujemy aktualną wartość natężenia koloru czerwonego, zmienimy ją, a na końcu ustawimy wartość natężenia komponentu niebieskiego tak, by była równa tej dla zielonego.




(pixelex1a)

Sprawdź swoją wiedzę

iter-9-1: Jaki kolor opisuje piksel o wartościach RGB (50, 0, 0)?





Obiekty klasy Image

Aby uzyskać dostęp do pikseli w rzeczywistym obrazie musimy na początku stworzyć obiekt klasy Image. Takie obiekty mogą być tworzone na dwa sposoby. Przede wszystkim obiekt Image może być tworzony na bazie plików przechowywujących cyfrowe obrazy. Obiekt klasy Image posiada atrybuty odpowiadające szerokości, wysokości oraz dla całego zbioru pikseli.

Można też stworzyć obiekty klasy Image który będzie „pusty”. Obiekt klasy EmptyImage posiada szerokość i wysokość. Wszystkie piksele należące do takiego obrazu będą „białe”.

Możemy uzyskać rozmiary obiektu Image stosując metody getWidth oraz getHeight. Można też uzyskać informacje o pikselu z konkretnej jego lokalizacji poprzez metodę getPixel oraz zmienić własności piksela z danej lokacji używając metody setPixel.

Opis do klasy Image znajdziecie poniżej. Pierwsze dwie linijki są przykładem tworzenia obiektu klasy Image. Parametry inicjalizacji instancji będą różne w zależności od tego czy użyjemy istniejącego pliku czy tworzymy pusty obraz.

Nazwa metody Przykład Wyjaśnienie
Image(filename) img = image.Image(„cy.png”) Tworzy obiekt Image z pliku cy.png.
EmptyImage() img = image.EmptyImage(100,200) Tworzy obiekt Image, w którym wszystkie piksele będą „białe”
getWidth() w = img.getWidth() Zwraca szerokość obrazu w pikselach.
getHeight() h = img.getHeight() Zwraca wysokość obrazu w pikselach.
getPixel(col,row) p = img.getPixel(35,86) Zwraca piksel w kolumnie 35 i rzędzie 86.
setPixel(col,row,p) img.setPixel(100,50,mp) Ustawia poksel w kolumnie 100 i rzędzie 50 na mp.

Rozważmy obraz pokazany poniżej. Założymy, że obrazek przechwoywany jest w pliku o nazwie „luther.jpg”. W drugiej linii otwieramy plik i wykorzystujemy jego zawartość do stworzenia obiektu o nazwie img. Mając już obiekt klasy Image, możemy użyć powyższych metod aby uzyskać informacje o właśnie stworzonym obiekcie lub użyć jakiś piksel i sprawdzić natężenia jego kolorów podstawowych.




(pixelex1)

Kiedy uruchomimy program zobaczymy, że obraz ma szerokość 400 pikseli i wysokoć 244 pikseli. Zobaczymy też, że piksel znajdujący się w kolumnie 45 oraz rzędzie 55 ma natężenia RGB równe 165, 161 oraz 158. Wypróbuj piksele w innych lokalizacjach, zmieniając argumenty metody getPixel i ponownie uruchamiając program.

Sprawdź swoją wiedzę

iter-9-2: Użyj powyższego przykładu ActiveCode i wybierz odpowiedź, która jest najbliższa wartościom RGB dla piksela w rzędzie nr 100 i kolumnie nr 30. Wartości mogą różnić się o 1 czy 2 w zależności od przeglądarki.





Obróbka obrazów oraz iteracje zagnieżdżone

Obróbka obrazów odnosi się do umiejętności manipulowania poszczególnymi pikselami obrazu cyfrowego. Aby przetworzyć wszystkie piksele, musimy systematycznie odwiedzić wszystkie rzędy i kolumny obrazu. Najlepiej użyć do tego iteracji zagnieżdżonych.

Iteracje zagnieżdżone oznaczają po prostu taką konstrukcję, gdzie jedna iteracja znajduje się wewnątrz drugiej. Nazwiemy je odpowiednio iteracją zewnętrzną i iteracją wewnętrzną. Aby zobaczyć jak to działa, proszę zerknąć na poniższy przykład.

for i in range(5):
    print(i)

Widzieliśmy to już wystarczającą ilość razy, by wiedzieć, że zmienna i przjmie kolejno wartości 0, potem 1, później 2, itd. aż do 4. Komenta print wywołana zostanie przy każdym obrocie pętli. Jednak ciało pętli może zawierać dowolne polecenia, zatem też inną iterację (kolejne pętlę for). Np.

for i in range(5):
    for j in range(3):
        print(i, j)

Pętla for i jest iteracją (pętlą) zewnętrzną, a foj j wewnętrzną. Każdy obrót pętli zewnętrznej wywoła pełną realizację pętli wewnętrznej – od początku do końca. Oznacza to, że w wyniku tych zagnieżdżonych iteracji dla każdej wartości jaka przyjmie i wyświetlą się wszystkie wartości zmiennej j.

Poniżej znajdziecie ten sam przykład, ale w ActiveCode. Wypróbujcie go. Postarajcie się zauwazyć, że wartości i pozostają niezmienne wtedy gdy wartości j się zmieniają. Iteracja wewnętrzna w efekcie obraca się szybciej od zewnętrznej.




(nested1)

Jeszcze inaczej możemy to sobie obejrzeć, badając szczegóły tego kodu w codelens. Przejdźcie poprzez tą iterację krok po kroku, by zrozumieć przepływ instrukcji, jaki tworzy się w przypadku pętli zagnieżdżonych. Po raz kolejny, dla każdej wartości zmiennej i, wszystkie wartości j będą wyświetlone. Możecie też zauważyć, że pętla wewnętrzna się zakończy zanim przejdziecie do kolejnego obrotu pętli zewnętrznej.

(nested2)

Naszym celem przy obróbce obrazu jest odwołanie się do każdego piksela. Do odwołania się do każdego rzędu pikseli użyjemy iteracji. W środku tej pętli użyjemy iteracji zagnieżdżonej, aby odwołać się do każdej kolumny. W rezultacie otrzymamy iterację zagnieżdżoną, podobną do tej powyżej, taką gdzie zewnętrzna pętla przetwarzać będzie rzędy od zerowego aż do wysokości obrazu (z jej wyłączeniem). Wewnętrzna pętla for odwoła się do każdej kolumny w danym rzędzie, znów od zerowej aż do maksymalnej szerokości obrazu (ponownie z jej wyłączeniem).

Wynikowy kod będzie wyglądał następująco. Teraz już możemy robić z pikselami co tylko chcemy.

for row in range(img.getHeight()):
    for col in range(img.getWidth()):
        # zrob cos z pokselem na pozycji (col,row)

Jeden z najprostszych algorytmów obróbki cyfrowej zdjęć to stworzenie negatywu obrazu. Negatyw oznacza, że po prostu, że każdy nowy piksel będzie odwrotnością swojego oryginału. Ale co to znaczy „odwrotny”?

W modelu RGB możemy rozumieć odwrotność komponentu czerwonego jako różnicę pomiędzy oryginalną wartością natężenia komponentu czerwonego i wartością maksymalną 255. Na przykład, jeżeli oryginalnie natężenie czerwonego wynosiło 50 to odwrotna, negatywna wartość czerwonego powinna wynieść 255-50, czyli po prostu 205. Oznacza to, że negatyw obrazu, na którym jest dużo czerwieni będzie miał tej czerwieni mało, a nagatywy pikseli z nikłym natężeniem czerwonego będą miały go dużo. Tak samo postąpimy dla niebieskiego i zielonego.

Program poniżej jest implementacją takiego algorytmu dla znanego już pliku (luther.jpg). Uruchom go, aby zobaczyć jak wygląda wynikowy negatyw obrazka. Zauwaz proszę, że mamy sporo obróbki danych, więc proces może zakończyć się dopiero po kilku sekundach. Dodatkowo mamy jeszcze dwa inne obrazki, które można użyć (cy.png i goldygopher.png).

cy.png

goldygopher.png

Zmień nazwę pliku w wywołaniu image.Image(), aby zobaczyć jak te obrazy wyglądają jako negatywy. Zauważ też, że na samym końcu widnieje metoda exitonclick, która to zamknie okno, kiedy na nie klikniecie. Umożliwi to „czyszczenie ekranu” przed obróbką kolejnego negatywu.




(acimg_1)

Spójrzmy nieco uważniej na program. Po zaimportowaniu modułu image tworzymy dwa obiekty klasy image. Pierwszy img odnosi się do zwykłego obrazu cyfrowego. .. Drugi newimg to pusty obrazek, który „wypełnimy” przekształcając obraz oryginalny .. piksel po pikselu. Drugi win to obiekt klasy ImageWin, służący do łatwiejszego wyświetlania obrazów w systemach Windows. Zauważcie, że szerokość i wysokość opakowania win jest taka sama jak oryginalnego obrazu cyfrowego.

W 7 i 8 linii znajdziecie iteracje zagdzieżdżone, które omawialiśmy wcześniej. Umożliwi nam to odniesienie się do każdego piksela w obrazku. W linii 9 pobieramy informację o danym pikselu.

W liniach 11-13 tworzymy nagatywy natężeń poprzez wydobycie oryginalnych wartości natężeń dla danego piksela i późniejsze odjęcie ich od 255. Kiedy obliczymy już newred, newgreen i newblue (odpowiednio nowe natężenia kolorów czerwonego, zielonego i niebieskiego), możemy stworzyć nowy piksel (Linia 15).

Na samym końcu musimy podmienić stary piksel naszego obrazu na nowy. Ważne jest, aby nowo przekształcony piksel wylądował w tym samym miejscu co piksel oryginalny.

Spróbuj przepisać program tak, aby zewnętrzna pętla iterowała po kolumnach, a wewnętrzna po rzędach. Dalej będziemy generowali negatyw obrazu, ale zobaczycie, że piksele będą aktualizować się w nieco innej kolejności.

Inne możliwości manipulowania pikselami

Istnieje wiele algorytmów przetwarzania obrazów, które mają taki sam wzorzec jak ten właśnie omówiony. To znaczy: pobierz informację o danym pikselu, wyodrębnij natężenia czerwony, zielony i niebieski, a następnie stwórz nowy piksel z nich. Nowy piksel powinniśmy umieścić w nowym obrazie tym samym miejscu, w którym znajdował się piksel oryginalny.

Można na przykład stworzyć skalę szarości poprzez uśrednianie natężeń czerwonego, zielonego i niebieskiego, a następnie użycie owej wartości dla wszystkich natężeń.

Ze skali szerości można stworzyć skalę czarno-białą poprzez ustawienie pewnego progu i wybierać czy wstawić pixel biały, czy czarny do pustego obrazka.

Można też posłużyć się arytmetyką zespoloną i stworzyć całkiem ciekawe efekty, takie jak tonowanie sepią

Właśnie osiągnałeś bardzo ważny punkt w nauce programowania w języku Python. Pomimo tego, że jest jeszcze wiele więcej rzeczy, które będziemy robić, to już opanowałeś podstawowe cegiełki, które są niezbędne do rozwiązania wielu interesujących problemów. Z punktu widzenia jesteście w stanie zaprogramować instrukcje warunkowe (wybór) oraz iteracje (pętle). Umiecie też rozbijać zagadnienia na podproblemy, dla tych ostatnich konstruować funkcje i w końcu wywoływać owe funkcje. Teraz skupimy się na nieco lepszej reprezentacji zagadnień poprzez pewne struktury danych, którymi to możemy manipulować. Omówimy sobie teraz zbiory danych, które możemy znaleźć w języku Python.

Sprawdź swoją wiedzę

iter-9-3: Co wydrukuje poniższa pętla zagnieżdżona? (Jeżeli masz kłopoty z tym zadaniem wróć do CodeLens 3).

for i in range(3):
    for j in range(2):
        print(i, j)
a.

0 0
0 1
1 0
1 1
2 0
2 1

b.

0   0
1   0
2   0
0   1
1   1
2   1

c.

0   0
0   1
0   2
1   0
1   1
1   2

d.

0   1
0   1
0   1





iter-9-4: Jak będzie wyglądał obraz wyprodukowany przez ActiveCode numer 16, jeżeli podmienimy następujące linijki:

newred = 255 - p.getRed()
newgreen = 255 - p.getGreen()
newblue = 255 - p.getBlue()

takimi:

newred = p.getRed()
newgreen = 0
newblue = 0





Następna część - Samodzielne przetwarzanie obrazów.