2011-08-28

Korekcja gamma

Pod tym terminem rozumiemy ogół zagadnień związanych z zapisywaniem wartości RGB w sposób nielinearny, ich kodowaniem do postaci nielinearnej oraz ich odtworzeniem na ekranie w postaci liniowej.

Historycznie do zapisu wartości RGB używało się bajtów. Jednakże 8 bitów to zbyt mało by liniowo oddać płynne przejścia jasności. Tak naprawdę potrzeba na to około 12 bitów. Z drugiej strony oko ludzkie jest bardziej czułe na zmiany ciemnych odcieni niż jasnych. Równomiernie rozłożone według ludzkiej percepcji wartości od ciemnego do jasnego w rzeczywistości odpowiadają funkcji potęgowej. Właśnie taką funkcję i jej odwrotność wykorzystuje się do kodowania i dekodowania informacji o jasności każdej składowej RGB.

Funkcja kodująca ma postać: $C'=C^{\gamma}$, gdzie $\gamma > 1$. Funkcja dekodująca ma postać: $C=C'^{\displaystyle{^{1}/_{\gamma}}}$. Mówiąc o kodowaniu i dekodowaniu gamma wartości wyjściowe jak i wyjściowe to liczby w zakresie 0-1, gdzie zero to czerń, zaś jeden to biel.
Z wykresu funkcji widać, że jedna czwarta jasności jest kodowana za pomocą połowy wartości.

Kodowanie i dekodowanie gamma stało się koniecznością wraz z pojawieniem się systemów zdolnych reprodukować kolor 24-bitowy (najpierw z palety). I ponieważ do dziś większość systemów używa do reprodukcji kolorów 24-bitów, one także kodują gammę.

sRGB w postaci 24-bitowej to nie tylko kompresja gamma, to także zapisanie jej w postaci ustalonej skali. Kiedy cały obraz jest ciemny, ale ciągle mamy wysoką czułość lub po prostu go ściemniliśmy i zapisaliśmy w postaci JPG. Informacja o dynamice kolorów ulega zmniejszeniu. To samo się dzieje kiedy zarejestrowane zostały obszary jasne (słońce) i ciemne cienie. Zakodowanie sceny o tak dużej dynamice jasności wymaga specjalnych algorytmów (Tone mapping), by nie utracić wrażenia dynamiki sceny. Scena już tak zapisana do JPG traci informację o początkowej jasności. Dlatego w programach graficznych powinniśmy całe przetwarzanie realizować i zapamiętywać na liczba dużej precyzji (najlepsza wydaje się liczba zmiennoprzecinkowa 32-bitowa). Istnieją także formaty pozwalające na zapis i odczyt takich obrazów. Słowo klucz to HDR (High-dynamic range). Ponieważ pamięć, łącza są coraz szybsze jest szansa, że w przyszłości większość materiałów będzie kodowała jasność na większej ilości bitów niż 8. HDR niekoniecznie oznacza brak kodowania gamma.

Gamma miała jeszcze jedną użyteczną cechę. Monitory CRT, ich część odpowiedzialna za emisję elektronów, także pracuje nieliniowo w funkcji bardzo zbliżonej do dekodującej gamma. W każdym razie kodowanie gamma nie ma związku z CRT, jakby się mogło wydawać, tylko ze zmieszczeniem pełnej informacji o jasności w bajcie. Współcześnie LCD także muszą uwzględnić fakt, że sygnał jest poddany kompresji gamma.

Kodowanie gamma obecnie jest dla nas prawie transparentne, dzięki standardowi jakie dla niego przyjęto. Typową wartością $\gamma$ jest 2.2. Używana była ona w kamerach, telewizji, systemach komputerowych. Wierna reprodukcja kolorów wymaga by wszystkie urządzenia po drodze używały takiej samej funkcji do kodowania gamma i jej odwrotności do dekodowania. Dotyczy to całego procesu przetwarzania który może wyglądać tak: kamera, mpeg, system, karta graficzna, złącze, monitor. Dlatego powstał standard sRGB, opisujący między innymi kodowanie gamma. Jest on współcześnie wykorzystywane tak powszechnie, że myśląc o kodowaniu RGB mamy na myśli sRGB.

Wspomniany wcześniej współczynnik gamma $\gamma=2.2$ został zaadaptowany przez sRGB. Obie metody kodowania są bardzo podobne do siebie ale nie identyczne. sRGB używa innego kodowania by uniknąć problemów numerycznych w okolicach zera i by zwiększyć dokładność numeryczną na niektórych systemach. Różnice są bardzo niewielkie w każdym bądź razie, stosując standardową funkcję potęgową nie popełnilibyśmy zbyt dużego błędu.

Funkcja kodują gamma ma postać:

$C=\begin{cases}
12.92C_{cases} & C_{LINEAR} \leqslant 0.0031308 \\
1.055{C_{LINEAR}}^{\displaystyle\frac{1}{2.4}} & C_{LINEAR} > 0.0031308
\end{cases}$
Dekodująca:

$C_{LINEAR}=\begin{cases}
\frac{C_{SRGB}}{12.92} & C_{SRGB} \leqslant 0.04045 \\
{(\frac{C_{SRGB}+0.055}{1.055}})^{2.4} & C_{SRGB} > 0.04045
\end{cases}$
Funkcje te w porównaniu do standardowej potęgowej o wykładniku 2.2 posiadają takie same wartości w zerze i w jeden. Funkcje sRGB w punktach 0.0031308 i 0.04045 są ciągłe, a ich pochodne są sobie równe.

Implementacja w kodzie:
public static class Gamma
{
    private const float A = 0.055f;
    private const float PHI = 12.92f;
    private const float GAMMA = 2.4f;

    private static float[] s_decode_table = new float[256];

    static Gamma()
    {
        for (int i = 0; i < 256; i++)
            s_decode_table[i] = DecodeGamma(i / 255.0f);
    }

    public static float EncodeGamma(float a_value)
    {
        if (a_value <= 0.0031308f)
            return a_value * PHI;
        else
            return (1 + A) * (float)Math.Pow(a_value, 1 / GAMMA) - A;
    }

    public static float DecodeGamma(byte a_value)
    {
        return s_decode_table[a_value];
    }

    public static float DecodeGamma(float a_value)
    {
        if (a_value <= 0.04045)
            return a_value / PHI;
        else
            return (float)Math.Pow((a_value + A) / (1 + A), GAMMA);
    }
}

Podczas wykonywania operacji arytmetycznych na wartościach kolorów (oświetlenie, resampling, filtrowanie tekstur) musimy pamiętać by te operacje wykonywać na kolorze liniowym. Inaczej do obrazu wynikowego wprowadzimy przekłamania kolorów. Ujawnią się one szczególnie na małych jasnych obszarach (resampling), gdzie ulegną one zwężeniu i podczas raytracingu na dużych obszar jednolicie podświetlonych.

Brak komentarzy:

Prześlij komentarz