dikamilo.net

Kolejny blog w sieci...

Modele barw oraz implementacja w C#

Dzisiaj będzie pierwszy wpis z dziedziny grafiki komputerowej, inspirowany laboratorium jakie miałem w poprzednim semestrze. Zostaną omówione pokrótce systemy barw takie jak RGB, CMYK, HSL, HSV, YUV, LAB oraz zostanie przedstawiona implementacja algorytmów konwersji między RGB a innymi modelami. Implementacja została wykonana w C# ze względu na dość wygodne i szybkie tworzenie systemu UI. Nie zostanie opublikowany pełny kod źródłowy projektu ze względu na dość chaotyczny sposób w jaki go realizowałem, nie wszystko zostało dobrze przemyślane i niektóre rzeczy nie są napisane zbyt optymalnie ale fragmenty które zostaną zaprezentowane w zupełności wystarczą do omówienia tematu.

Interfejs użytkownika aplikacji prezentuje się następująco:

Image Processing UI

W następnych wpisach systematycznie będę omawiać kolejne elementy programu. Bitmapę którą użyłem można znaleźć w serwisie deviantart.com.

Bitmapy w C#

Generalnie do pracy z bitmapami będą nam potrzebne dwa pakiety, System.Drawing oraz System.Drawing.Imaging. Pierwszy z nich jest domyślnie dodawany przy tworzeniu nowej klasy. Na nasze potrzeby będziemy zajmować się tylko bitmapami typu JPG oraz BMP to nam w zupełności wystarczy.

Aby załadować bitmapę z dysku do naszego programu skorzystamy z klasy OpenFileDialog:

using System.Drawing;
using System.Drawing.Imaging;

Bitmap bitmapa;
OpenFileDialog openDialog = new OpenFileDialog();
// wyłączamy możliwość zaznaczenia kliku plików
openDialog.Multiselect = false; 
// filtrujemy tylko JPG oraz BMP
openDialog.Filter = "JPG files (*jpg)|*jpg| BMP files (*bmp)|*bmp";

if (openDialog.ShowDialog() == DialogResult.OK)
{
    bitmapa = new Bitmap(openDialog.FileName);
    // tutaj można się pokusić o konwersję do innego formatu PixelFormat
}

Do pikseli bitmapy możemy się dobrać głównie na dwa sposoby. Za pomocą funkcji bitmap.getPixel(x,y), bitmap.setPixel(x,y) lub za pomocą wskaźników. Pierwszy sposób jest dość prosty i sprowadza się do podania współrzędnych pożądanego piksela, w zamian otrzymujemy obiekt Color który przechowuje reprezentację RGB danego piksela. Niestety metoda ta jest dość powolna i strasznie nie optymalna np. przy nakładaniu filtrów splotowych na dużych bitmapach.

Drugi sposób jest bardziej wydajny ale wymaga od nas trochę wysiłku. Po pierwsze musimy zablokować obszar bitmapy w pamięci za pomocą funkcji LockBits. Funkcja ta zwraca nam obiekt klasy BitmapData z którego będziemy mogli wyciągnąć potrzebne nam dane. Następnie musimy utworzyć wskaźnik. C# przy stosowaniu wskaźników wymaga od nas abyśmy wszystkie operacje na wskaźnikach umieszczali w bloku unsafe a co za tym idzie w ustawieniach projektu musimy pozwolić na "unsafe code block" w naszym programie.

// blokujemy cały obszar bitmapy do odczytu i zapisu
BitmapData dane = bitmapa.LockBits(
    new Rectangle(0, 0, bitmapa.Width, bitmapa.Height), 
    ImageLockMode.ReadWrite, bitmapa.PixelFormat);

unsafe
{
    // tworzymy wskaźnik na pierwszy piksel
    byte* bitmapaPtr = (byte*)dane.Scan0;
    // padding bitmapy
    int offset = dane.Stride - dane.Width * 3;

    for (int y = 0; y < bitmapa.Height; y++)
    {
        for (int x = 0; x < bitmapa.Width; x++)
        {
             // operacje na pikselu

            // następny piksel
            bitmapaPtr += 3;
        }
        // następny wiersz
        bitmapaPtr += offset;
    }
}

Offset obliczamy dlatego że w C# bitmapy muszą mieć wymiary o potędze liczby 2 a co za tym idzie jak wczytamy obrazek o niestandardowych wymiarach to zostanie dodany odpowiedni padding. Stride zwraca nam całkowitą długość wiersza w bajtach wiec odjęcie od niej długości bitmapy razy 3 piksele da nam padding.

Całość operacji opiera się na iteracji po wszystkich pikselach oraz przesunięcia wskaźnika na następny co realizuje pętla for na powyższym listingu.

Do wyświetlenie bitmapy w UI użyjemy kontrolki PictureBox dostępnej w Toolboksie. Przypisanie bitmapy do kontrolki realizowane jest w następujący sposób:

picturebox.Image = bitmapa;

Model RGB

Model RGB jest wykorzystywany między innymi przez monitory, telewizory czy aparaty cyfrowe. Został opracowany na podstawie właściwości ludzkiego oka. Składa się z trzech kanałów, Czerwony, Zielony oraz Niebieski. Każdy z kanałów przedstawia kolor w przedziale od 0 do 255. Ustawienie trzech kanałów na 255 daje nam kolor biały natomiast ustawienie na 0 daje kolor czarny. Każdy kanał zazwyczaj zapisuje się na 8 bitach co daje nam 24bity na piksel. Pracując z bitmapami RGB należy pamiętać że C# reprezentuje ten model w BGR.

To co chciałbym tutaj pokazać to wyświetlanie każdego kanału osobno. Osobiście zrobiłem trzy bitmapy na kanał, każda przechowuje odpowiedni kanał RGB w odcieniu szarości, Aby uzyskać taki efekt należy wszystkie składowe wynikowego obrazu ustawić na taką samą wartość.

for (int y = 0; y < bitmap.Height; y++)
{
    for (int x = 0; x < bitmap.Width; x++)
    {
        // tworzenie trzech bitmap separujacych kanał RGB
        redPtr[0] = redPtr[1] = redPtr[2] = bitmapPtr[2];
        greenPtr[0] = greenPtr[1] = greenPtr[2] = bitmapPtr[1];
        bluePtr[0] = bluePtr[1] = bluePtr[2] = bitmapPtr[0];

        redPtr += 3;
        greenPtr += 3;
        bluePtr += 3;
        bitmapPtr += 3;
    }

    redPtr += offset;
    greenPtr += offset;
    bluePtr += offset;
    bitmapPtr += offset;
}

Wynik jest następujący, każda bitmapa jest wyświetlana osobno:

Model RGB

Model CMYK

CMYK jest zestawem czterech podstawowych kolorów farb drukarskich, Cyan - odcień niebieskiego, Magneta - połączenie czerwieni i niebieskiego, Yellow - żółty oraz Black - czarny. Kanały RGB są w przedziałach 0 - 255 natomiast wynikowe kanały CMYK są w przedziałach 0 - 1 a co za tym idzie aby wyświetlić kanał w odcieni szarości należy go pomnożyć przez 255 a następnie zaokrąglić do liczby całkowitej.

RGB na CMYK:

for (int y = 0; y < bitmap.Height; y++)
    {
        for (int x = 0; x < bitmap.Width; x++)
        {
            float cyan = 1.0f - ((int)bitmapPtr[2] / 255.0f);
            float magneta = 1.0f - ((int)bitmapPtr[1] / 255.0f);
            float yellow = 1.0f - ((int)bitmapPtr[0] / 255.0f);
            float black = Math.Min(cyan, Math.Min(magneta, yellow));

            if (black >= 1.0f)
            {
                cyan = magneta = yellow = 1.0f;
            }
            else
            {
                float s = 1.0f - black;
                cyan = (cyan - black) / s;
                magneta = (magneta - black) / s;
                yellow = (yellow - black) / s;
            }

            cyanPtr[0] = cyanPtr[1] = cyanPtr[2] = 
                (byte)(Math.Round(cyan * 255.0f));

            magnetaPtr[0] = magnetaPtr[1] = magnetaPtr[2] = 
                (byte)(Math.Round(magneta * 255.0f));

            yellowPtr[0] = yellowPtr[1] = yellowPtr[2] = 
                (byte)(Math.Round(yellow * 255.0f));

            blackPtr[0] = blackPtr[1] = blackPtr[2] = 
                (byte)(Math.Round(black * 255.0f));

            // wskaźniki += 3
        }

        // wskaźniki += offset
    } 
}

CMYK na RGB:

for (int y = 0; y < bitmap.Height; y++)
    {
        for (int x = 0; x < bitmap.Width; x++)
        {
            cyan = (cyan * (1 - black) + black);
            magneta = (magneta * (1 - black) + black);
            yellow = (yellow * (1 - black) + black);

            // Mix CMY channels to RGB
            bitmapPtr[0] = (byte)((1 - yellow) * 255);
            bitmapPtr[1] = (byte)((1 - magneta) * 255);
            bitmapPtr[2] = (byte)((1 - cyan) * 255);

            // wskaźniki += 3
        }

        // wskaźniki += offset
    } 
}

Moja funkcja konwertująca generuje kanały CMYK oraz je miesza do RGB. Nie wydzielałem mieszania do RGB do osobnej funkcji ze względu na to że każdy kanał mam w osobnej bitmapie, a co za tym idzie zamiana liczby zmiennoprzecinkowej na całkowitą powoduje straty przy konwersji, nie uzyskamy takiego samego obrazu jak oryginał. Lepszym pomysłem było by przechowywanie wartości CMYK w tablicy float. Mój program miał za zadanie jedynie konwertować modele i je wyświetlać więc nie brałem pod uwagę możliwości operacji na kanale przed konwersją do RGB.

Model CMYK

Model YUV

YUV był stosowany w czasach przechodzenia z telewizorów czarno białych na kolorowe. Kanał Y przechowuje jasność obrazu a kanały UV jego barwę. Należy zaznaczyć że wartości kanały Y są w przedziale [0, 1] natomiast U [-0.436, 0.436] oraz V [-0.615, 0.615].

RGB na YUV:

for (int y = 0; y < bitmap.Height; y++)
{
    for (int x = 0; x < bitmap.Width; x++)
    {
        double red = imagePtr[2] / 255.0;
        double green = imagePtr[1] / 255.0;
        double blue = imagePtr[0] / 255.0;

        double luma = 0.299 * red + 0.587 * green + 0.114 * blue;

        double u = -0.1471376975169300226 * red - 0.2888623024830699774 
            * green + 0.436 * blue;

        double v = 0.615 * red - 0.5149857346647646220 * green - 
            0.1000142653352353780 * blue;

        lumaPtr[0] = lumaPtr[1] = lumaPtr[2] = (byte)(luma* 255);
        uPtr[0] = uPtr[1] = uPtr[2] = (byte)(((u + 0.436) / 0.872 ) * 255);
        vPtr[0] = vPtr[1] = vPtr[2] = (byte)(((v + 0.615) / 1.23)* 255);

        // wskaźniki += 3
    }

    // wskaźniki += offset
}

YUV na RGB:

for (int y = 0; y < bitmap.Height; y++)
{
    for (int x = 0; x < bitmap.Width; x++)
    {
        red = ((luma + 1.139837398373983740 * v) * 255.0);

        green = ((luma - 0.3946517043589703515 * u -0
            0.5805986066674976801 * v) * 255.0);

        blue = ((luma + 2.032110091743119266 * u) * 255.0);

        bitmapPtr[0] = (byte)(blue);
        bitmapPtr[1] = (byte)(green);
        bitmapPtr[2] = (byte)(red);

        // wskaźniki += 3
    }

    // wskaźniki += offset
}

Model YUV

Model HSV, HSL oraz LAB

Ze względu na to że listingi z implementacją tych modeli są dość długie zostaną one pominięte. Algorytmy konwersji można znaleźć na stronie easyRGB.

Modele HSL i HSL są bardzo do siebie podobne. Kanał H - Hue (odcień) jest taki sam w obu modelach i jest zapisywany w stopniach w przedziale [0, 360]. Kanał S - Saturation (nasycenie) jest reprezentowany w procentach tak samo jak kanał V - Value oraz L - Lightness.

Model HSL Model HSV

Model HSL jest stosowany między innymi w oknie Hue/Saturation w Photoshopie. W moim programie również posiadam taką funkcjonalność, jednak ze względu że wartości są przeliczane do bitmap nie działa to zbyt wydajnie.

Model LAB jest najbardziej złożony obliczeniowo. Przy zapisywaniu każdego kanału jako osobnej bitmapy, konwersja potrafi potrwać kilka-kilkanaście sekund w zależności od wielkości obrazu. LAB jest często stosowany przez profesjonalnych grafików do retuszowania zdjęć.

Model LAB

Tagi

Komentarze (4)

  • #1 // baltek71 napisał:

    Chyle czoła, że po tylu godzinach spędzonych przed grafiką i C#, masz ochotę i siłę tłumaczyć to innym. Duży +. Co do całego programu to sam miałem wielkie ambicje stworzyć mini-photoshopa, chęci były jednak czasowo nie wyrobiłem. Dla chętnych, wrzucam binarke:

  • #2 // Kamil Łuszczki napisał:

    Dzięki za sharing. Też miałem zamiar zrobić z tego mały edytor/ program użytkowy do grafiki i przepisać całość do C++ w wxWidgets albo QT ale też nie mam czasu na to.

  • #3 // Ultimion napisał:

    a moze mi ktos wyslac ta grafike na maila albo podac tutaj dokladnie link do tego przykladowego zdjecia... chcialbym porownac wynik moich algorytmow z wynikami tutaj przedstawionymi :)

  • #4 // Kamil Łuszczki napisał:

    Dobrym materiałem będzie dowolny program graficzny, Photoshop czy Gimp :) Co do grafiki to można ją pobrać z http://nuahs.deviantart.com/art/The-Old-Farm-70488282