Kawałek Kodu

Mam taką książkę z roku 1989 o grafice komputerowej. Jest to jedna z tych książek, której treść jest aktualna również dziś, a jednocześnie rzadko spotykana w nowych pozycjach. Jest tam przedstawiona twarz w porównaniu dwóch rozdzielczości - niskiej i wysokiej. Aby grafika odwzorowywała realny obraz użyto ditheringu. W tamtych czasach przy ograniczeniach, metoda robiła na mnie ogromne wrażenie. Ostatnio wróciłem do niej i zrobiła jeszcze większe wrażenie.

Była sobie kropka.

Tak naprawdę to ten wpis miał być o metodzie ditheringu według Floyda-Steinberga. Celem było osiągnięcie efektu tego ditheringu bez użycia JavaScript, a z pomocą SVG. To jednak okazało się niemożliwe ze względu na algorytm, który potrzebuje dodatkowego bufora na obliczenia błędów dyfuzji i mnożenia wartości. Ale przecież istnieje nie tylko ta metoda ditheringu. Istnieją też metody:

  • obcinania (bardziej znana pod nazwą posteryzacji czy thresholdingu),
  • oparte na wzorcu,
  • oparte na losowym wzorcu,
  • uporządkowane oparte o macierz tonalną,
  • oparte na błędzie dyfuzji (te felerne).

Niezły bajer.

Metodę Bayer'a widziałem już wcześniej w działaniu choć nie wiedziałem, że tak się nazywa. Częściej spotykałem ją pod nazwą "ordered dithering". Nazwa ta stanowi nie tylko określenie tej metody, ale grupy (uporządkowanych metod), w której zawarty jest również dithering halftone (ten możesz pamiętać z ilustracji ze starych czarno-białych periodyków). Jeśli nie kojarzysz jak wygląda dithering Bayer'a, to może pamiętasz obrazy poddane tej metodzie gdzie przejścia kolorów przypominają kratkę lub gwiazdki? Nie? Nie przejmuj się - za chwilę kilka przykładów.

Zerknijmy na pseudokod:

for y
  for x 
   newvalue := (pixel[x][y] + matrix[x mod 4][y mod 4]) / 2
   if (newvalue < threshold)
     newpixel[x][y] := 0
   else
     newpixel[x][y] := 255
   endif

W tablicy matrix (tu 4x4, ale może być 3x3, 8x8, 16x16, itd.) przechowujemy wartości tonalne (szarości) dla potrzeby działania algorytmu. Przesuwając się po każdym pikselu obrazu źródłowego dodajemy do niego jasność z macierzy. Sumę dzielimy przez dwa. W tym momencie obraz będzie wyglądał podobnie do źródłowego, ale z nałożoną delikatną siatką, która w pewnych miejscach podjaśni piksele, a w innych ściemni. Druga część kodu robi 50% pozostałego efektu. W zależności od wartości threshold (przy maksymalnej wartości składowej 255, najodpowiedniejsza jest wartość 128), piksel zmieniony przez wartość macierzy albo podbijamy do bieli (czy też do maksimum wartości składowej koloru jeśli modyfikujemy obraz kolorowy), albo wyłączamy całkowicie.

Bazową macierzą jest macierz 2x2, kolejne powstają w wyniki jej rekursji. Jest ona rozciągana na większy obszar, a nowe wartości sąsiadów stanowią mniej zróżnicowane wartości tonalne. Można rzec, że większa macierz jest mniej restrykcyjna dla obszaru, na którym działa.
Macierz 2x2 wygląda tak (nie ma informacji jaka jest logika w takim ustawieniu wartości, ale widocznie autor miał jakiś zamysł):

1 2
3 0

Maksymalna wartość w tym przypadku wynosi 3. Łatwo wywnioskować, że stanowi ona: rozmiar_macierzy2-1.

Macierz 4x4 ma taką postać:

0 12 3 15
8 4 11 7
2 14 1 13
10 6 9 5

I kolorystyczna wizualizacja (macierz jest obrócona o 180° w wyniku działania algorytmu, o którym niżej, ale nie ma to kompletnie znaczenia dla jej działania):

2x2:

   
   

4x4:

       
       
       
       

8x8:

               
               
               
               
               
               
               
               

Iteracyjny algorytm do generacji macierzy znalazłem w kilku miejscach w sieci, ale jeden z nich okazał się najprostszy do implementacji w PHP (równie łatwo możesz go przekształcić na inny język).

$matrix = [];
$size = 4;

function dithervalue($x, $y, $size) {
    $d = 0;

    while ($size-- > 0) {
        $d = ($d << 1 | ($x & 1 ^ $y & 1)) << 1 | $y & 1;
        $x >>= 1;
        $y >>= 1;
    }
    return $d;
}

for ($i = 0; $i < $size * $size; $i++) {
    $matrix[$i] = dithervalue((int) ($i / $size), $i % $size, $size >> 1);
}

Ponieważ będziemy operować na składowych koloru o wartoścach maksymalnych 255, to wartości w macierzy należy też sprowadzić do tego maksimum. Wcześniej znaleźliśmy wzór dla maksimum w macierzy, to sprowadzenie wartości do 255 będzie przebiegać według wzoru:

nowa_wartosc = 255 * wartosc / (maksimum_w_macierzy)

Dla macierzy 2x2 otrzymamy więc wartości:

85 170
255 0

Gdybyśmy nie zastosowali macierzy, a tylko drugą część algorytmu, otrzymalibyśmy obrazek wynikowy poddany ditheringowi z pierwszej grupy. Nakładając macierz, można powiedzieć, że zwiększamy lub zmniejszamy szansę danego piksela na jego utrzymanie lub zniknięcie w obrazie wynikowym. Dwa sąsiednie piksele niewiele różniące się jasnością bez użycia macierzy prawdopodobnie zniknęłyby lub pozostały. Jednak jeśli nałożymy macierz, to różnicujemy ich szanse na przeżycie lub zniknięcie. W ten sposób na wyniku jeden z nich pozostanie (jako jasna kropka), ale drugi stanie się czarny, lub odwrotnie.

Podobnie jak w przypadku prób z metodą Floyda-Steinberga, tu też zacząłem od SVG. Ale w pewnym momencie stwierdziłem, że to co mogę osiągnąć za pomocą filtrów SVG, mogę osiągnąć również z pomocą filtrów CSS. I tak jak wspomniałem we wstępie, ostateczny efekt wraz z jego prostotą i możliwościami użycia wprawił mnie w osłupienie. To było jakbym pierwszy raz zobaczył dithering! Tak było!

A więc do rzeczy. Jako macierzy tonalnej możemy użyć gradientu w SVG, gradientu w CSS lub po prostu pliku PNG. Użyjemy tego ostatniego, aby nie zaciemniać CSS. Mając macierz trzeba ją jakoś nałożyć na obraz źródłowy. Nie ma się co martwić o pobieranie kolejnych wartości z macierzy, bo tą (plik PNG) podłożymy jako powtarzające się tło, odpada więc obawa o liczenie modulo. Jak dodać do siebie obraz i macierz? Nakładając je na siebie zobaczymy tylko macierz. Dlatego też macierz nałożymy z opacity=0.5. W tym momencie uzyskamy efekt, o którym pisałem wcześniej - obraz z nałożoną delikatną siatką. Dzielenie sumy przez 2 możemy pominąć. Nie mamy jak tego wykonać i nie musimy się przejmować, że składowe kolorów przekroczą wartość 255.

Na razie otrzymaliśmy to, czyli obraz z nałożoną siatką:

Połowa za nami. Jak wykonać jednak warunek w CSS, tak aby piksele poniżej wartości 128 zgasły, a powyżej pozostały? Tu z pomocą przychodzi nam filtr kontrastu. Wystarczy ustawić wartość na 100 i sprawa załatwiona! Ciemnie piksele staną się czarne jak smoła, jasne staną się jasne jak Słońce.

Poniżej efekt odpowiednio dla macierzy 4x4, 8x8, oraz 16x16:

HTML + CSS odpowiadające za ten prosty i efektowny efekt:

<div>
  <img src="./mario-1557240_640.jpg"/>
</div>
/* kontener z naszym
   obrazkiem
*/
div {
  position: relative;
  display: inline-block;

/* filtr kontrastu
   zastępuje nam
   instrukcję IF
*/
  filter: contrast(100);
}

/* macierz tonalna
*/
div:after {
  content: '';
  display: block;
  width: 100%;
  height: 100%;
  left: 0;
  top: 0;
  position: absolute;

/* na obrazek nakładamy
   macierz tonalną
*/
  background: url(bayermatrix4x4.png);

/* dzięki opacity 50%
   sumuje się ona z obrazem
   pod nią
*/
  opacity: 0.5;
}

Mały tip: jeśli nie chcesz, aby macierz była wygładzana na ekranach z gęstością piksela większą niż 1, należy dodać do CSS:

image-rendering: pixelated;

Żeby być uczciwym muszę przyznać, że filtr kontrastu nie załatwi w pełni tego co robi instrukcja IF (druga połowa algorytmu). Nie uzyskujemy 8 kolorów, a tyle powinniśmy uzyskać maksymalizując każdą składową koloru do wartości 255 (#000000, #0000FF, #00FF00, #00FFFF, itd.). Przysłużyć może się dodatkowy filtr saturacji z wartością również 100. Jeśli chcesz go dodać, pamiętaj aby zrobić to za filtrem kontrastu (kolejność ma tu znaczenie!). Przy czym mi w takim przypadku udało się uzyskać 10 unikalnych kolorów w przykładowym obrazie. Swoją drogą polecam Ci wpis: Podróż do wnętrza kineskopu, czyli emulacja RGB.

Jeśli moja drobna nieuczciwość nie zniechęciła Cię do czytania dalej i/lub powyższy efekt nie wprawił Cię w osłupienie, to co powiesz na dithering wideo?

Nic nie stoi na przeszkodzie, aby filtr stosować również na zwykłych elementach HTML:

Można również dodać filtr saturacji z wartością 0 i uzyskamy... uwaga! To:

Przyznam, że ta metoda ma w sobie coś magicznego bez względu na sposób implementacji. Połączenie z pozoru nieuporządkowanej macierzy tonalnej z obrazem źródłowym generuje uporządkowane, "zditherowane" gradienty. A co lepsze, pomimo powtarzającego się wzorca (macierzy), wcale nie widać na wyniku artefaktów w miejscu łączenia się krawędzi wzorca.

To już ostatnia kropka. Do przeczytania w kolejnym magiczym wpisie!

 

Przydatne linki:
Iteracyjna metoda generacji macierzy Bayer'a (1).
Iteracyjna metoda generacji macierzy Bayer'a (2).
Rekurencyjna metoda generacji macierzy Bayer'a.
Emulacja RGB.
Powtórka z tabliczki mnożenia, czyli o przesunięciach bitowych.