artykuły

Delphi - Transformacje grafiki - Przyjaśnianie i przyciemnianie

21:36
nie, 9 styczeń 2005
Artykuł opisuje zagadnienie programowej zmiany jasności obrazów.

Wstęp

Niemal we wszystkich programach graficznych, czy to w małych, szybkich przeglądarkach, czy też w profesjonalnych programach do obróbki grafiki istnieją funkcję pozwalające nam na szybką korektę zdjęcia. Jeśli piszemy własną przeglądarkę, w niej również nie może zabraknąć takich filtrów. Postaramy się dzisiaj napisać filtr, który umożliwi nam przyciemnianie i przyjaśnianie obrazka (przetwarzanie bitmap). Dowiemy się też jak można przyspieszyć nieco działanie takiego filtra.

Piszemy program

  1. Na początku musimy zgromadzić potrzebne materiały. Do tego celu będziesz musiał przygotować przykładowy obrazek. Musi być to bitmapa. Najlepiej niech to będzie mała bitmapa ;) Powiedzmy (do 640x480).
  2. Uruchom Delphiego ;)
  3. Umieść na formie dwa przyciski (paleta Standard),SpinEdit(paleta Samples) orazOpenPictureDialog(paleta Dialogs).
  4. W Inspektorze Obiektów zmień właściwośćCaptionprzyciskuButton1na "Zastosuj", a przyciskuButton2na "Otwórz obrazek".
  5. Przejdź do Edytora Kodu (F12) i odszukaj globalną sekcjęvar(umieszczona jest ona zaraz przed sekcją implementation). Dodaj do niej następującą linijkę:bitmapa : TBitmap;Zadeklarowałeś właśnie globalnie bitmapę (globalnie to znaczy, że pamięć tej zmiennej nie będzie usuwana po zakończeniu procedury). Nasza zmienna jest typuTBitmap, czyli jest klasą. Należy więc ją teraz utworzyć (jeśli deklarujemy klasę (najczęściej zmienna z przedrostkiem T) to musimy je potem utworzyć (przed jej wykorzystaniem).
  6. Przejdź na sam koniec modułu (kodu). Zatrzymaj się tuż przed słówkiemend.(z kropką na końcu). Wpisz tam takie cztery linie: initialization bitmapa := TBitmap.Create; finalization bitmapa.Free; Są to kolejnoinitialization- odpowiadająca za wykonywanie operacji przed właściwym rozpoczęciem programu (występuje przed zdarzeniemOnCreate!). My zaraz przy starcie program tworzymy w niej naszą zmienną "bitmapa" typuTBitmap. Jeśli byśmy tego nie zrobili wówczas wystąpiłby błąd, ponieważ nie można odwołać się do nie utworzonej klasy.
    Druga z kolei sekcja to analogiczniefinalization. Jak nietrudno się domyślić - odpowiada ona za wykonywanie instrukcji tuż przed zakończeniem programu.
  7. Przejdź teraz na sam początek modułu i do sekcjiusesdodaj słówkoMath.
  8. Przejdź teraz do formularza (F12) i dwukrotnie kliknij na przycisk który ma otwierać obrazek (Button2). Pojawi się Edytor kodu z nowo wygenerowaną procedurą obsługującą zdarzenieOnClick;) Wpisz tam coś takiego:If not OpenPictureDialog1.Execute Then Exit;W wolnym tłumaczeniu na polski wyszłoby nam coś takiego: "Jeśli okno otwierania obrazka nie jest uruchomione to zakończ procedurę". Rozumiecie coś z tego ? ;) Przyjmujemy, że ta linia po prostu otworzy nam okno, które pozwoli nam na wybór obrazka.
  9. Ponownie przejdź do formularza (F12) i dwukrotnie kliknij na przyciskButton1(ten z etykietą "Zastosuj"). W oczekującej na wypełnienie procedurze ;) zadeklaruj takie zmienne (sekcjavarprzedbegin): var x, y : integer; kolor : TColor; r,g,b : byte; swiatlo : integer; następnie, po słówkubeginwypełnij następująco: swiatlo := SpinEdit1.Value; bitmapa.LoadFromFile(OpenPictureDialog1.FileName); Form1.Canvas.Draw(0,0,bitmapa); for y := 0 to bitmapa.Height-1 Do for x := 0 to bitmapa.Width-1 Do begin kolor := bitmapa.Canvas.Pixels[x,y]; r := Min(Max(GetRValue(kolor)+swiatlo,0),255); g := Min(Max(GetGValue(kolor)+swiatlo,0),255); b := Min(Max(GetBValue(kolor)+swiatlo,0),255); bitmapa.Canvas.Pixels[x,y] := RGB(r,g,b); end; Form1.Canvas.Draw(0,0,bitmapa);
  10. Wszyscy wszystko rozumieją, prawda? ;)
    W pierwszej linii przypisujemy do zmiennej "swiatlo" wartość podaną przez użytkownika w kontrolceSpinEdit1. Zmienna od tej pory będzie przechowywała wartość o którą mamy przyciemnić obrazek.
    Druga linia ładuje do zmiennej "bitmapa" obrazek wskazany przez użytkownika. Oczywiście gdy użytkownik nie wskaże wcześniej żadnego pliku (przez naciśnięcie na przycisk "Otwórz obrazek") wystąpi błąd ;)
    Trzecia linia rysuje oryginalny obrazek na formularzu w pozycji (0,0), (pozycja (0,0) oznacza iż lewy, górny róg obrazka będzie namalowany właśnie w tym miejscu).
    Czwarta i piąta linia to typowa już dla filtrów, wcześniej omawiana konstrukcja, przechodząca kolejno przez wszystkie piksele obrazka.
    Szósta linia to bardzo przydatne słówkobegin;)
    Siódma linia pobiera kolor aktualnie rozpatrywanego piksela.
    Ósma, dziewiąta i dziesiąta linia rozkładają kolor na poszczególne składowe, odpowiednio je modyfikując (dodając do każdej składowej odpowiednią wartość i  w ten sposób odpowiednio ją przyjaśniając lub przyciemniając w wypadku gdy zmienna "swiatlo" jest ujemna). Szczegóły tych trzech linii omówimy później.
    Dziewiąta linia rysuje piksel o zmodyfikowanym przez nas kolorze.
    Dziesiąta linia kończy nam pętle.
    Jedenasta linia wyświetla na formie już zmodyfikowany obrazek.

    Teraz może powiemy sobie co nieco więcej o tych tajemniczych trzech liniach. Weźmy sobie jedną z nich :r := Min( Max( GetRValue( kolor ) + swiatlo, 0), 255 );Będziemy rozpatrywać tę linię po kolei zaczynając od środka.GetRValue( kolor );zwraca nam składową R z koloru znajdującego się w zmiennej "kolor". Wiecie z poprzednich artykułów, że składowe są typu byte, tak więc mogą przyjmować wartości w zakresie [0..255]. I tym sposobem na kolor składają nam się trzy składowe R,G,B (R - czerwony G - zielony B - niebieski). Jeśli wszystkie składowe ustawimy na ich maksymalną wartość (255) to otrzymamy kolor biały. Ale gdy składowym zostanie przypisana wartość większa od maksymalnej możliwej (np. 256) to nie dostaniemy w zamian tabliczki z błędem, ale składowa zacznie liczyć od początku i otrzymamy 0. Analogicznie przypisując składowej liczbę o dwa większą od jej wartości maksymalnej (czyli 257), w wyniku otrzymamy 1 itd., itd...
    Teraz jeśli rozkładamy kolor biały (wszystkie składowe mają wartość 255), czyli R = 255 to po przyjaśnieniu obrazka oSWIATLO= 50 otrzymalibyśmy 49. Stałoby się tak ze wszystkimi składowymi (ponieważ wszystkie są równe po 255) w efekcie otrzymalibyśmy kolor (49,49,49) czyli kolor zbliżony do czarnego. Aby tak się nie działo i po przyjaśnieniu białego o 50 nadal otrzymywalibyśmy biały musimy zastosować ograniczenie.
    FunkcjaMAXzwraca nam większą z dwóch liczb podanych jako parametr, a funkcjaMINodwrotnie, zwraca mniejszą z dwóch liczb podanych jako parametr (zastosowałem ograniczenia w obie strony gdyż po przypisaniu zmiennej typubytewartości ujemnej, której nie może ona przyjąć np. -1 zmienna typu byte przyjmuje wartość 255, po przypisaniu -2 przyjmuje 254 itd, itd).
    Załóżmy, że rozpatrujemy kolor biały (R = 255) i zmiennaSWIATLO= 30.Min( Max( 255+30, 0 ), 255 );Funkcja Max(255+30, 0) zwróci nam wartość 285, czyli nie zmieni wyniku. Po zwróceniu wyniku przez funkcjeMaxzostaje uruchomiona funkcja Min( 285, 255 ). Sami powiedzcie co jest mniejsze - 285 czy 255? Oczywiście że 255, dlatego zwrócona zostaje wartość 255. Tym sposobem ograniczenie zadziałało. Teraz to samo ograniczenie tylko w inną stronę - na liczby ujemne: Załóżmy, że rozpatrujemy kolor czarny (R = 0) i zmiennaSWIATLO= -30.Min( Max( 0-30, 0 ), 255 );Funkcja Max(-30, 0) zwróci nam wartość 0. Po zwróceniu wyniku przez funkcjeMaxzostaje uruchomiona funkcja Min( 0, 255 ) która nie zmieni wyniku. Ostatecznym wynikiem działania ograniczenia będzie 0.

    Warto zapamiętać ten układ funkcjiMiniMax- w filtrach często się przydaje...

Jak przyspieszyć działanie filtru?

Najszybszy filtr możemy wykonać operując na palecie kolorów. W ten jednak sposób nie uzyskamy wielu innych efektów. Co więc zrobić jeśli chcemy aby filtr działał dużo szybciej? Po pierwsze, zaraz nauczymy się jak korzystać z funkcji ScanLine, która baaardzo przyspieszy operacje (operuje ona bowiem na danych bezpieśrednio przez przesunięcie za pomocą wskaźników). Po drugie, Delphi posiada bardzo użyteczną funkcje, o której wie zapewne niewielu. Niektóre zmienne da się oznaczyć jako zmienne z których będziemy często korzystać. Kompilator próbuje je wtedy usadowić w wewnętrznej pamięci procesora ("próbuje", bo nie zawsze mu się to udaje). Jak wiemy - wewnętrzna pamięć procesora to często najszybsza z możliwych pamięci. Jest ona bardzo mała (liczona w kB) i nie możemy jej przypisać bitmap, czy innych "dużych" obiektów. Możemy natomiast przyspieszyć wykonywanie filtru przez umieszczenie w niej najczęściej używanych zmiennych - w naszym przypadkux, y, kolor, swiatlo, r, g, b... Spróbujmy więc:
Wystarczy jedynie zmienić deklarację tych zmiennych dodając po typie zmiennej słówko "register":

var x, y : integer register; kolor : TColor register; r,g,b : byte register; swiatlo : integer register;

Proste? Chyba aż za proste ;) Od tej pory filtr powinien wykonywać się nieco szybciej (zwłaszcza na starszych komputerach, gdzie była duża rozbieżność pomiędzy szybkością pamięci operacyjnej oraz pamięci procesora). Nic jednak tak nie przyspiesza operacji na grafice jak...

ScanLine - stukrotne przyśpieszenie ;)

Funkcja skanowania linii potrafi kilkadziesiąt razy przyspieszyć operacje na grafice w stosunku do szybkości jaką uzyskaliśmy stosując standardową metodę. Zdecydowałem się opisać ją dopiero teraz, gdyż mniej doświadczeni użytkownicy mogliby nie "załapać" sensu jej używania nie znając podstaw.
Spójrzmy więc na nasz kawałek kodu, służący do zmiany jasności obrazka z perspektywy użycia ScanLine. Najpierw jednak przepiszmy sekcjęvarw naszej procedurze (Button1Click): var x, y : integer; swiatlo : integer; linia : PByteArray; Jak widzimy zniknęły zmienne r,g,b oraz zmienna kolor - nie będą nam już dłużej potrzebne. Pojawiła się natomiast zmienna linia, której typ PByteArray wygląda przerażająco ;) PByteArray jest wskaźnikiem do typu TByteArray, służącego do uzyskiwania dostępu do dynamicznie alokowanych tablic. Typ ten znajduje się w module SysUtils. Nie tylko wyglądało strasznie, ale i takie było, co? ;) Postaram się jakoś to wy(po)jaśnić. Przygotowałem w tym celu skromny obrazek na którym widzimy naszą tablicę. Budowa bitmapy a tablica wskaźników W zmiennej "linia" (a właściwie w tablicy zmiennych o nazwie "linia") przechowywane są referencje (czyli dowiązania, skróty) do poszczególnych składowych koloru w danym wierszu (tak na marginesie: do jednej zmiennej może być przypisanych kilka referencji - jest to mechanizm analogiczny do skrótów do plików - jednemu plikowi możemy przypisać dodolną ilość skrótów do niego). Tak więc nasza tablica o nazwie "linia" przechowuje dany wiersz obrazka. Możemy modyfikować jego poszczególne składowe poprzez modyfikacje naszej tablicy. Referencje do składowych obrazka uzyskujemy przez wywołanie funkcji ScanLine(), która zwraca je do naszej tablicy. Robimy to następująco:linia := ScanLine[y];gdzie y to numer wiersza, który chcemy mieć w naszej tablicy.
Zobaczmy zatem jak wygląda całość procedury, po zmianach: swiatlo := SpinEdit1.Value; bitmapa.LoadFromFile(OpenPictureDialog1.FileName); Form1.Canvas.Draw(0,0,bitmapa); for y := 0 to bitmapa.Height-1 Do begin linia := bitmapa.ScanLine[y]; x := 0; while (x < (bitmapa.Width-1)*3) Do begin linia[x+2] := Min(Max(linia[x+2]+swiatlo,0),255); linia[x+1] := Min(Max(linia[x+1]+swiatlo,0),255); linia[x+0] := Min(Max(linia[x+0]+swiatlo,0),255); Inc(x,3); end; end; Form1.Canvas.Draw(0,0,bitmapa); Jak widzimy, nasz kod uległ zmianie. Zmodyfikowane nie zostały jedynie trzy pierwsze linie, oraz linia ostatnia. Już tłumaczę:
W pierwszej linii przypisujemy do zmiennej "swiatlo" wartość podaną przez używkownika w kontrolceSpinEdit1. Zmienna od tej pory będzie przechowywała wartość o którą mamy przyciemnić obrazek.
Druga linia ładuje do zmiennej "bitmapa" obrazek wskazany przez użytkownika. Oczywiście gdy użytkownik nie wskaże wcześniej żadnego pliku (przez naciśnięcie na przycisk "Otwórz obrazek") wystąpi błąd ;)
Trzecia linia rysuje oryginalny obrazek na formularzu w pozycji (0,0), (pozycja (0,0) oznacza iż lewy, górny róg obrazka będzie namalowany właśnie w tym miejscu).
Czwarta linia: to pętla, która przechodzi nam przez wszystkie wiersze naszego obrazka.
Piąta linia: to begin
Szósta linia: utworzenie skrótu do zawartości danego wiersza (y) obrazka. Siódma linia: Z racji, że zaraz będziemy korzystać z pętli typu innego niż for, musimy wyzerować zmienną liczącą pętli.
Ósma linia: to pętla, która przechodzi przez wszystkie piksele obrazka, skacząc o 3 składowe za każdym razem. W każdym kroku tej pętli modyfikujemy trzy różne elementy tablicy (dokładniej: składowe RGB). Dziewiąta linia: znów begin
Dziesiąta, jedenasta i dwunasta linia: modyfikacja tablicy składowych danego wiersza "y". Z racji, że ułożenie wartości w tablicy odpowiada budowie bitmapy (składowe ułożone są w sposób BGR a nie RGB - czyli odwrotnie). Jeśli więc patrzyć z punktu widzenia naszej pętli while, która skacze o 3 składowe w każdym kroku, to początkowo x=0 więc zapis linia[0] oznacza składową B, zapis linia[0+1] oznacza składową G, a zapis linia[0+2] oznacza składową R. W każdym kolejnym kroku (x zwiększamy o 3 w linii trzynastej) znów: linia[3] oznacza składową B, linia[3+1] składową G, a linia[3+2] składową R. Możemy więc zapisać, że linia[x] będzie zawsze oznaczała składową niebieską (B), linia[x+1] składową zieloną (G), a linia[x+2] składową czerwoną (R). Wiedząc już o działaniu konstrukcji dwóch funkcji Min(Max(skladowa,0),255) z poprzednich akapitów, domyślamy się, że linia:linia[x+2] := Min(Max(linia[x+2]+swiatlo,0),255);odpowiada następującej linii kodu, z procedury wolniejszej, opartej o GetRValue() i inne wynalazki:r := Min(Max(GetRValue(kolor)+swiatlo,0),255);Oczywiście nie ma wątpliwości, który zapis jest czytelniejszy, to który wybierzesz zależy tylko i wyłącznie od Ciebie. Pytanie tylko: czy zależy Ci na czytelności kodu, czy na szybkości jego działania. Szesnasta linia (ostatnia): Następuje narysowanie przetworzonej bitmapy na płótnie formularza, w pozycji (0,0), a więc w lewym, górnym rogu.

Przejrzystość zapisu kodu, a nakłady pamięciowe i długość kodu

Sami przyznacie, że kod którym się posługujemy nie jest zbyt przejrzysty ze względu na zastosowaną tablicę. Czy nie lepiej byłoby się odwoływać do konkretnego np. czerwonego subpiksela poprzez linia[x].R ? Cóż pozostaje? Zmodyfikujmy więc nasz kod. Ostrzegam jednak, że takie rozwiązanie zajmuje wiele więcej pamięci ze względu na zadeklarowaną nie-dynamiczną tablicę i nie obsługuje bitmap których szerokość jest powyżej zakresu Word (czyli 65535 ;).
Niestety nie udało mi się na razie zastosować powyższego sposobu dla tablic dynamicznych (co zminimalizowałoby pamięciożerność do poziomu poprzednich przykładów) przez co, póki co, napiszemy to w sposób podobny do tego jaki można odnaleźć w starych Pascalowych źródłach.
Zacznimy więc... Aby uprościć (?) nieco zapis kodu, dodajmy deklarację trzech nowych typów przed słówkiemvarnaszej procedury oraz zmieńmy typ zadeklarowanej zmiennej linia z PByteArray na PTriByteArray. Całość deklaracji powinna to wyglądać tak: procedure TForm1.Button1Click(Sender: TObject); type TRGB = record b,g,r : byte; end; TTriByteArray = array[Word] of TRGB; PTriByteArray = ^TTriByteArray; var x, y : integer; swiatlo : integer; linia : PTriByteArray; begin ... Pierwszy typ, TRGB zawiera trzy zmienne typu byte, co ważne, zadeklarowane w takiej kolejności, w jakiej mamy je poukładane w bitmapie, w więc BGR (Niebieski, Zielony, Czerwony). Kolejny typ to typ tablicowy złożony z 65536 (zakres typu Word to 0..65535) elementów. Tymi elementami są paczki rekordów TRGB (a więc nasze składowe). Kolejny typ, PTriByteArray, jest typem wskaźnikowym, wskazującym na nasz typ tablicowy. No i... zminiliśmy jeszcze typ zmiennejlinia- jest ona teraz typu PTriByteArray, a więc wskazuje nam na tablicę TTriByteArray. Coraz mniej mi się to podoba, ponieważ miało być uproszczenie kodu - a wyszło... sami wiecie... Może choć uda uprościć się sekcję kodu... zobaczymy.
Dzięki uprzednim deklaracjom, kod nie zmieni się znacząco. Dzięki nim, zamiast pisać linia[x] będziemy pisać linia[x].B, zamiast linia[x+1] będziemy pisać linia[x].G, natomiast zamiast linia[x+2] napiszemy linia[x].R. Musimy zmienić również pętle, ponieważ teraz posuwamy się co jeden element (nie tak jak robiliśmy to poprzednio - co trzy). Najodpowiedniejsza będzie pętlafor. Zobaczmy więc jak zmieni się kod (podaje przykład całej procedury Button1Click):
procedure TForm1.Button1Click(Sender: TObject); type TRGB = record b,g,r : byte; end; TTriByteArray = array[Word] of TRGB; PTriByteArray = ^TTriByteArray; var x, y : integer; swiatlo : integer; linia : PTriByteArray; begin swiatlo := SpinEdit1.Value; bitmapa.LoadFromFile(OpenPictureDialog1.FileName); Form1.Canvas.Draw(0,0,bitmapa); for y := 0 to bitmapa.Height-1 Do begin linia := bitmapa.ScanLine[y]; for x:=0 To bitmapa.Width-1 Do begin linia[x].R := Min(Max(linia[x].R+swiatlo,0),255); linia[x].G := Min(Max(linia[x].G+swiatlo,0),255); linia[x].B := Min(Max(linia[x].B+swiatlo,0),255); end; end; Form1.Canvas.Draw(0,0,bitmapa); end; Podsumowując, nie jestem pewien czy uproszczenie odwoływania się do tablicyliniawartało tyle zachodu i pamięci. To, który kod zastosujecie zależy od Was.

Zakończenie

Dzięki wszystkim za uwagę! Na końcu chciałem dodać, że stosując metodę ScanLine jesteśmy uzależnieni od głębi koloru wczytanego obrazu. Artykuł pokazuje jak operować na najpopularniejszych obecnie, obrazach 24-bitowych (a więc 8-bitów na każdy z kanałów R,G,B). W podany sposób nie zmodyfikujemy bitmap mających 256, czy 16 kolorów (takimi bitmapami prawdopodobnie kiedyś się zajmiemy). Więc, jeśli w którymś z przykładów pojawia Ci się błąd - prawdopodobnie wczytujesz obraz o i innej niż 24-bity głębi kolorów. Jeśli jednak koniecznie chcesz wczytać taką bitmapę, można w prosty sposób skonwertować ją do trybu 24-bitowego. Wystarczy dopisać jedną instrukcję, przed jakąkolwiek modyfikacją obrazu: ... swiatlo := SpinEdit1.Value; bitmapa.LoadFromFile(OpenPictureDialog1.FileName); bitmapa.PixelFormat := pf24bit; // konwersja bitmay do 24-bitow Form1.Canvas.Draw(0,0,bitmapa); ... Do artykułu dołączam kod źródłowy sposobu pierwszego oraz kod źródłowy sposobu opartego o ScanLine a także kod źródłowy sposobu opartego o ScanLine na "przejrzystym" kodzie. Życzę wszystkim doskonałych i ciekawych filtrów ;)

12345
Delphi - Transformacje grafiki - Przyjaśnianie i przyciemnianie Autor opinii: Czytelnicy, data przesłania: 0

Podobne artykuly:

Skomentuj

Aby zamieścić komentarz, proszę włączyć JavaScript - niestety roboty spamujące dają mi niezmiernie popalić.






Komentarze czytelników

    • Dzejkob
    • czw, 4 marzec 2010, 22:04
    • Dzięki za artykuł. Przydał się.
    • kaka
    • nie, 8 marzec 2009, 20:44
    • no stronka ekstra:) pozdrawiam
Dexter