Skoči na vsebino

Obdelava slik1

Poudarjanje robov (angl. Edge enhancement) je slikovni filter, ki poveča kontrast robov. Rob je definiran kot znatna lokalna sprememba v intenziteti slike. Pri idealnem robu (angl. step) se sprememba intenzitete zgodi v koraku ene slikovne točke. Idealni robovi so v slikah zelo redki, še posebej, če slike prej gladimo, da iz njih odstranimo šum. Spremembe intenzitete se zato zgodijo v nekaj zaporednih slikovnih točkah, takemu robu pravimo nagib (angl. ramp). Filter za poudarjanje robov deluje tako, da na sliki prepozna ostre robove (na primer rob med motivom in ozadjem) in poveča kontrast slike na območju neposredno okoli roba. Zaradi tega je rob videti bolj definiran. Spodnja slika prikazuje primer poudarjanja robov:

Vhodna slika Izhodna slika
Vhodna slika Izhodna slika

Za postopek poudarjanja robov uporabimo Laplacovo jedro velikosti 3x3. Vsako slikovno točko in njeno okolico v izvorni sliki pomnožimo z Laplacovim jedrom, produkte seštejemo in vsoto uporabimo za vrednost slikovne točke v novi sliki s poudarjenimi robovi. Temu procesu pravimo konvolucija. Spodnja slika prikazuje en korak konvolucije z Laplacovim jedrom.

Edge enhancement

Na sliki je prikazano Lapacovo jedro, ki ga bomo uporabili za poudarjanje robov na sliki. Vidimo, kako poudarimo vrednost slikovne točke z vrednostjo 2, ki tvori rob s slikovno točko na svoji desni (vrednost 0). Novo vrednost, 6, dobimo tako, da jedro položimo na izvorno sliko, zmnožimo istoležne elemente ter seštejemo delne produkte.

Delo s slikami

Za branje slik z diska v pomnilnik ter zapisovanje nazaj na disk bomo uporabljali javno dostopno knjižnico STB. Knjižnica je dokaj obsežna, vendar bomo uporabili le funkciji stbi_load in stbi_write, ki sta definirani v zaglavjih stb_image.h in stb_image_write.h. Navodila za uporabo funkcij najdemo tukaj.

Predstavitev slik v pomnilniku

Sliko bomo najprej iz datotek na disku prebrali v pomnilnik. Slike so sestavljene iz množice slikovnih točk. Pri sivinskih slikah je običajno ena slikovna točka predstavljena z osmimi biti, ki določajo sivinski nivo (od črne do bele). Slikovna ravnina dimenzije Width x Height pa je v pomnilniku predstavljena kot vektor slikovnih točk, pri čemer do posamezne slikovne točke dostopamo na naslednji način: image[Y x Width + X]. Dostop do posamezne slikovne točke prikazuje spodnja slika.

Naslavljanje slikovnih točk - en kanal

Količine X (stolpec), Y (vrstica), Width (širina) in Height (višina) so izražene v slikovnih točkah.

Pri barvnih slikah je tipično vsaka slikovna točka predstavljena s 4 x 8 biti ali štirimi bajti. Prvi trije bajti določajo tri komponente barve (angl. RGB, Red, Green in Blue), medtem ko pri določenih zapisih (na primer png) zadnji bajt določa prosojnost. Pravimo tudi, da je slika sestavljena iz štirih kanalov. Vsaka slikovna točka je torej v pomnilniku predstavljena s štirimi vrednostmi, pri čemer je vsaka vrednost zapisana z enim bajtom. Štirikanalno slika dimenzij Width x Height v pomnilniku prikazuje spodnja slika.

Naslavljanje slikovnih točk - več kanalov

Do posamezne slikovne točke sedaj dostopamo na naslednji način: image[(Y x Width + X) x cpp + channel], pri čemer je CPP število kanalov na slikovno točko (ang. channels per pixel) in je lahko med 1 in 4, channel pa je indeks kanala z vrednostjo od 0 do 3.

V nadaljevanju se bomo omejili na štirikanalne slike (četudi so sivniske) in jih bomo s funkcijo stbi_load zapisali v pomnilnik. V primeru sivinskih slik, bodo zadnji trije bajti slikovne točke neuporabljeni. Na prvi pogled je rešitev potratna, saj sivinske slike v pomnilniku zasedajo štirikrat več prostora, ampak bomo zaradi tega imeli isto kodo za obdelavo vseh vrst slik.

Ščepec za poudarjanje robov

Vsaka nit bo izračunala novo vrednost ene slikovne točke v sliki. V ta namen bo morala pri filtrih velikosti 3x3 dostopati še do osmih slikovnih točk v neposredni okolici slikovne točke, za katero računa novo vrednost. Pri izbranem Laplacovem jedru je dovolj, da dostopa samo do štirih sosednjih slikovnih točk, pri katerih ima jedro neničelne vrednosti. Ščepec za poudarjanje robov je prikazan spodaj.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//***************************************************
// Image sharpening using a 3x3 kernel; Source: https://setosa.io/ev/image-kernels/
//
//      |  0  -1   0 |
// K =  | -1   5  -1 |
//      |  0  -1   0 |
//
//***************************************************

// CUDA kernel for image sharpening. Each thread computes one output pixel
__global__ void sharpen(const unsigned char *imageIn, unsigned char *imageOut, const int width, const int height, const int cpp)
{
    // Get pixel
    int x = blockIdx.x * blockDim.x + threadIdx.x;
    int y = blockIdx.y * blockDim.y + threadIdx.y;

    if (x < width && y < height)
    {
        for (int c = 0; c < cpp; c++)
        {
            unsigned char px01 = getIntensity(imageIn, y - 1, x, c, height, width, cpp);
            unsigned char px10 = getIntensity(imageIn, y, x - 1, c, height, width, cpp);
            unsigned char px11 = getIntensity(imageIn, y, x, c, height, width, cpp);
            unsigned char px12 = getIntensity(imageIn, y, x + 1, c, height, width, cpp);
            unsigned char px21 = getIntensity(imageIn, y + 1, x, c, height, width, cpp);

            short pxOut = (5 * px11 - px01 - px10 - px12 - px21);
            pxOut = MIN(pxOut, 255);
            pxOut = MAX(pxOut, 0);
            imageOut[(y * width + x) * cpp + c] = (unsigned char)pxOut;
        }
    }
}

V zanki for se sprehodimo čez vse kanale in za vsak kanal posebej nit prebere vrednosti petih slikovnih točk iz slike: slikovne točke, za katero računa novo vrednost, ter njenih štirih sosedov (levo, desno, zgoraj in spodaj), kot to zahteva Laplacovo jedro. Za dostop do vrednosti posameznih slikovnih točk pripravimo funkcijo getIntensity:

1
2
3
4
5
6
7
8
9
__device__ inline unsigned char getIntensity(const unsigned char *image, int row, int col,
                                             int channel, int height, int width, int cpp)
{
    if (col < 0 || col >= width)
        return 0;
    if (row < 0 || row >= height)
        return 0;
    return image[(row * width + col) * cpp + channel];
}

Organizacijo niti nastavimo na naslednji način:

1
2
    dim3 blockSize(BLOCK_SIZE, BLOCK_SIZE);
    dim3 gridSize(ceil(width / blockSize.x), ceil(height / blockSize.y));

Niti se bodo izvajale v delovnih skupinah velikosti BLOCK_SIZE x BLOCK_SIZE. Celotno število niti je odvisno od velikosti vhodne slike in mora biti v obeh dimenzijah deljivo z velikostjo delovne skupine, v našem primeru 16. Ker dimenzije vhodnih slik niso nujno deljive s 16, pri določanju števila niti zaokrožimo velikost slike navzgor tako, da bo deljiva s 16.

Ščepec sharpen se na Nvidia Tesla V100 pri obdelavi slike velike 2580x1319 slikovnih točk izvede v 0,845 milisekunde:

$ srun --partition=gpu --gpus=1 prog helmet_in.png helmet_out.png
Loaded image helmet_in.png of size 2580x1319.
Kernel Execution time is: 0.845 miliseconds

Zgornja koda se nahaja v mapi repozitorija delavnice skupaj z navodili za prevajanje in zagon 07-image-filter.


  1. © Patricio Bulić, Davor Sluga, Univerza v Ljubljani, Fakulteta za računalništvo in informatiko. Gradivo je objavljeno pod licenco Creative Commons Priznanje avtorstva-Nekomercialno-Deljenje pod enakimi pogoji 4.0 Mednarodna