Kawałek Kodu

Wbrew pozorom dzisiejszy wpis nie będzie uczył technik samoobrony, nie będzie też kursem lutowania online. Pokażę Ci jak od środka działają popularne funkcje dostępne w programach graficznych, jak negatyw, rozjaśnianie czy posteryzacja. Opierają się na jednopunktowych operacjach graficznych. Jednopunktowych, bo wpływ na wartość wynikowego punktu obrazu, ma wartość tylko jednego punktu obrazu źródłowego.
Operację taką można opisać równaniem: w(x,y)=F(z(x,y)), gdzie x, y, to współrzędne obrazu, z obraz źródłowy, a w obraz wynikowy. F to operacja jednopunktowa.

Będziemy działać na tym zdjęciu:

Jak wygląda poprawny LUT?

LUT to skrót od look-up tables, czyli tablice wyszukiwania. Zamiast przeliczać na bieżąco wartość każdego piksela obrazu poddawanego danej funkcji, przeliczymy skończoną ilość wartości i wstawimy je do takiej tablicy. Załóżmy, że mamy obraz o 256 wartościach składowej. Potrzebna jest nam więc tablica LUT o 256 indeksach. Albo 3 jeśli chcemy przetwarzać każdą składową z osobna inną operacją jednopunktową. Każdy indeks tabeli możemy jednocześnie uznać za wartość piksela (lub składowej) obrazu źródłowego (indeks 0 - wartość 0, indeks 1 - wartość 1). Natomiast wartość pod danym indeksem tablicy będzie już wartością przetworzoną przez operację jednopunktową. Jeśli kolejne wartości w tablicy to 0-255, to będziemy mieć do czynienia z operatorem identyczności - obraz po jego przetworzeniu nie zmieni się.

Pamiętasz stare klisze fotograficzne?

Właśnie! Tam zawsze miałeś białe źrenice i czarne zęby. To jest operacja negacji. Dla koloru białego chcemy kolor czarny, dla czarnego biały. Tak jest na krańcach tabeli LUT, a idąc do środka zamieniamy ze sobą kolejne elementy. Jeśli nie jest to jasne, to przy założeniu 256 wartości, najprościej zapisać taką operację: w(x,y)=255-z(x,y).

Tablica LUT będzie wyglądać tak:

255 254 253 252 251 250 ... 4 3 2 1 0

I wykres:

Zobaczmy to w praktyce:

<canvas></canvas>

+

var img = new Image();
var canvas = document.querySelector('canvas');
var ctx = canvas.getContext('2d');
var imageData;
var i32;

ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false;

var LUT = [];

/* tworzymy tablicę LUT */
for(var i = 0; i < 256; i++){
    LUT[i] = 255 - i;
}

var offset = 0;

/* czekamy na załadowanie obrazka */
img.onload = function(){
    canvas.width = img.width;
    canvas.height = img.height;

    i32 = new Uint32Array(canvas.height * canvas.width)

    ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
    imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

    for (var y = 0; y < canvas.height; y++) {
      for (var x = 0; x < canvas.width; x++) {

        /* ponieważ imageData ma postać BBGGRRAA czyli 4 bajty
           liczymy sourceOffset z pozycji * 4
        */
        var sourceOffset = (x + y * imageData.width) << 2;
        var sourceR = imageData.data[sourceOffset + 2];
        var sourceG = imageData.data[sourceOffset + 1];
        var sourceB = imageData.data[sourceOffset];

        /* wartość składowej to jednocześnie indeks w tablicy LUT
           dla każdej składowej pobieramy odpowiadającą jej wartość z tablicy
           a całość składamy w postać AARRGGBB
        */
        i32[offset++] = 0xFF000000 | (LUT[sourceR] << 8 | LUT[sourceG]) << 8 | LUT[sourceB];
      }
    }

    var i8 = new Uint8ClampedArray(i32.buffer);
    imageData.data.set(i8);

    ctx.putImageData(imageData, 0, 0);  
};
img.src = './mario-1557240_640.jpg';

I wynik naszej negacji:

O operatorze posteryzacji raczej nie dowiedziałeś się z klisz, ale może właśnie ze starych posterów czy czasopism.

Jego działanie polega na zredukowaniu poziomów szarości (lub poziomów jasności kolorów). Obraz wynikowy wygląda wtedy jak pierwsze zdjęcia z epoki początku internetu - jest "plamiasty", widzimy spore połacie jednego koloru.
A jakie jest jego wnętrze? Dzielimy pulę wartości na tyle przedziałów ile zadajemy poziomów posteryzacji. Jeśli mamy 2 poziomy posteryzacji, to wartości 0-127 dostają wartość 64, a wartości 128-255 wartość 192. Czyli ze środka przedziału. Ale nic nie stoi na przeszkodzie, aby wartości te wybrać z początku przedziału, końca, lub inne, dowolne.

Tablica LUT dla posteryzacji przy 32 poziomach:

4 4 4 4 4 4 4 4 12 12 ... 252 252 252 252 252 252 252 252

Wykres posteryzacji dla 4 poziomów:

Generowanie tablicy LUT wygląda następująco (reszta kodu nie zmienia się):

var LUT = [];
var levels = 4;

for(var i = 0; i < 256; i++){
    LUT[i] = parseInt(i / (256 / levels)) * (256 / levels) + (256 / levels / 2);
}

W przykładzie mamy 4 poziomy. Oznacza to więc, że aby wartość 0-255 sprowadzić do takiej liczby poziomów, musimy ją podzielić przez 64 (256/4).
Uzyskamy wtedy:

i x=parseInt(i/64) y=x*64 z=y+32
(tu dodajemy wartość z połowy przedziału)
0-63 0 0 32
64-127 1 64 96
128-191 2 128 160
192-255 3 192 224

Tak będzie wyglądać posteryzacja na naszym obrazie:

Jeśli czujesz się coraz bardziej oświecony, to czas na operację regulacji jasności.

Chcąc uzyskać obraz jaśniejszy dodajemy do każej wartości założoną wartość X, chcąc uzyskać obraz ciemniejszy, odejmujemy. Pamiętajmy, aby wynik tej operacji sprowadzić do przedziału 0-255.

Jeśli chcemy rozjaśnić obraz o 25%, to tablica LUT ma postać:

64 65 66 67 ... 255 255 255 255 255 255 255

Wykres:

Kod do generowania tablicy LUT:

var LUT = [];

for(var i = 0; i < 256; i++){
    var j = i + 64;
    if(j > 255){
        j = 255;
    }
    LUT[i] = j;
}

Dodajemy do każdej wartości 64, czyli zwiększamy jasność o 25%.

Niech stanie się jasność!

Na dziś koniec LUTowania. Zachęcam Cię do przeszukania zasobów internetu i eksperymentowania z innymi operatorami lub z odrębnymi tablicami dla każdej składowej koloru piksela. Dla jasności - widzimy się w następnym wpisie!

 

Przydatne linki:
The Fast & The Canvas, czyli szybki dostęp do pikseli na Canvas
Negacja na canvas (z tego wpisu)
Posteryzacja na canvas
Jasność i canvas