Lekce 33 - Nahrávání komprimovaných i nekomprimovaných obrázků TGA

V lekci 24 jsem vám ukázal cestu, jak nahrávat nekomprimované 24/32 bitové TGA obrázky. Jsou velmi užitečné, když potřebujete alfa kanál, ale nesmíte se starat o jejich velikost, protože byste je ihned přestali používat. K diskovému místu nejsou zrovna šetrné. Problém velikosti vyřeší nahrávání obrázků komprimovaných metodou RLE. Kód pro loading a hlavičkové soubory jsou odděleny od hlavního projektu, aby mohly být snadno použity i jinde.

Začneme dvěma hlavičkový soubory. Texture.h, první z nich, popisuje strukturu textury. Každý hlavičkový soubor by měl obsahovat ochranu proti vícenásobnému vložení. Zajišťují ji příkazy preprocesoru jazyka C. Pokud není definovaná symbolická konstanta __TEXTURE_H__, nadefinujeme ji a do stejného bloku podmínky vepíšeme zdrojový kód. Při následujícím pokusu o inkludování hlavičkového souboru existence konstanty oznámí preprocesoru, že už byl soubor jednou vložen, a tudíž ho nemá vkládat podruhé.

#ifndef __TEXTURE_H__

#define __TEXTURE_H__

Budeme potřebovat strukturu informací o obrázku, ze kterého se vytváří textura. Ukazatel imageData obsahuje data obrázku, bpp barevnou hloubku, width a height rozměry. TexID je identifikátorem OpenGL textury, který se předává funkci glBindTexture(). Type určuje typ textury - GL_RGB nebo GL_RGBA.

typedef struct// Struktura textury

{

GLubyte* imageData;// Data

GLuint bpp;// Barevná hloubka v bitech

GLuint width;// Šířka

GLuint height;// Výška

GLuint type;// Typ (GL_RGB, GL_RGBA)

GLuint texID;// ID textury

} Texture;

#endif

Druhý hlavičkový soubor, tga.h, je speciálně určen pro loading TGA. Opět začneme ošetřením vícenásobného inkludování, poté vložíme hlavičkový soubor textury.

#ifndef __TGA_H__

#define __TGA_H__

#include "texture.h"// Hlavičkový soubor textury

Strukturu TGAHeader představuje pole dvanácti bytů, které ukládají hlavičku obrázku. Druhá struktura obsahuje pomocné proměnné pro nahrávání - např. velikost dat, barevnou hloubku a podobně.

typedef struct// Hlavička TGA souboru

{

GLubyte Header[12];// Dvanáct bytů

} TGAHeader;

typedef struct// Struktura obrázku

{

GLubyte header[6];// Šest užitečných bytů z hlavičky

GLuint bytesPerPixel;// Barevná hloubka v bytech

GLuint imageSize;// Velikost paměti pro obrázek

// GLuint temp;// Překl.: nikde není použitá

GLuint type;// Typ

GLuint Height;// Výška

GLuint Width;// Šířka

GLuint Bpp;// Barevná hloubka v bitech

} TGA;

Deklarujeme instance právě vytvořených struktur, abychom je mohli použít v programu.

TGAHeader tgaheader;// TGA hlavička

TGA tga;// TGA obrázek

Následující dvě pole pomohou určit validitu nahrávaného souboru. Pokud se hlavička obrázku neshoduje s některou z nich, neumíme ho nahrát.

GLubyte uTGAcompare[12] = { 0,0, 2,0,0,0,0,0,0,0,0,0 };// TGA hlavička nekomprimovaného obrázku

GLubyte cTGAcompare[12] = { 0,0,10,0,0,0,0,0,0,0,0,0 };// TGA hlavička komprimovaného obrázku

Obě funkce nahrávají TGA - jedna nekomprimovaný druhá komprimovaný.

bool LoadUncompressedTGA(Texture*, char*, FILE*);// Nekomprimovaný TGA

bool LoadCompressedTGA(Texture*, char*, FILE*);// Komprimovaný TGA

#endif

Přesuneme se k souboru TGALoader.cpp, který implementuje nahrávací funkce. Prvním řádkem kódu vložíme hlavičkový soubor. Inkludujeme pouze tga.h, protože texture.h jsme už vložili v něm.

#include "tga.h"// Hlavičkový soubor TGA

Funkce LoadTGA() je ta, kterou v programu voláme, abychom nahráli obrázek. V parametrech se jí předává ukazatel na texturu a řetězec diskové cesty. Nic dalšího nepotřebuje, protože si všechny ostatní parametry detekuje sama (ze souboru). Deklarujeme handle souboru a otevřeme ho pro čtení v binárním módu. Pokud něco selže, např. soubor neexistuje, vypíšeme chybovou zprávu a vrátíme false jako indikaci chyby.

bool LoadTGA(Texture* texture, char* filename)// Nahraje TGA soubor

{

FILE* fTGA;// Handle souboru

fTGA = fopen(filename, "rb");// Otevře soubor

if(fTGA == NULL)// Nepodařilo se ho otevřít?

{

MessageBox(NULL, "Could not open texture file", "ERROR", MB_OK);

return false;

}

Zkusíme načíst hlavičku obrázku (prvních 12 bytů souboru), která určuje jeho typ. Výsledek se uloží do proměnné tgaheader.

if(fread(&tgaheader, sizeof(TGAHeader), 1, fTGA) == 0)// Načte hlavičku souboru

{

MessageBox(NULL, "Could not read file header", "ERROR", MB_OK);

if(fTGA != NULL)

{

fclose(fTGA);

}

return false;

}

Právě načtenou hlavičku porovnáme s hlavičkou nekomprimovaného obrázku. Jsou-li shodné nahrajeme obrázek funkcí LoadUncompressedTGA(). Pokud shodné nejsou zkusíme, jestli se nejedná o komprimovaný obrázek. V tomto případě použijeme pro nahrávání funkci LoadCompressedTGA(). S jinými typy souborů pracovat neumíme, takže jediné, co můžeme udělat, je oznámení neúspěchu a ukončení funkce.

Překl.: Měla by se ještě testovat návratová hodnota, protože, jak uvidíte dále, funkce v mnoha případech vracejí false. Program by si bez kontroly ničeho nevšiml a pokračoval dále.

if(memcmp(uTGAcompare, &tgaheader, sizeof(tgaheader)) == 0)// Nekomprimovaný

{

LoadUncompressedTGA(texture, filename, fTGA);

// Překl.: Testovat návratovou hodnotu !!!

// if(!LoadUncompressedTGA(texture, filename, fTGA))// Test návratové hodnoty

// {

// return false;

// }

}

else if(memcmp(cTGAcompare, &tgaheader, sizeof(tgaheader)) == 0)// Komprimovaný

{

LoadCompressedTGA(texture, filename, fTGA);

// Překl.: Testovat návratovou hodnotu !!!

// if(!LoadCompressedTGA(texture, filename, fTGA))// Test návratové hodnoty

// {

// return false;

// }

}

else// Ani jeden z nich

{

MessageBox(NULL, "TGA file be type 2 or type 10 ", "Invalid Image", MB_OK);

fclose(fTGA);

return false;

}

Pokud dosud nenastala žádná chyba, můžeme oznámit volajícímu kódu, že obrázek byl v pořádku nahrán a že může z jeho dat vytvořit texturu.

return true;// Vše v pořádku

}

Přistoupíme k opravdovému nahrávání obrázků, začneme nekomprimovanými. Tato funkce je z velké části založena na té z lekce 24, moc novinek v ní nenajdete. Zkusíme načíst dalších šest bytů ze souboru a uložíme je do tga.header.

bool LoadUncompressedTGA(Texture* texture, char* filename, FILE* fTGA)// Nahraje nekomprimovaný TGA

{

if(fread(tga.header, sizeof(tga.header), 1, fTGA) == 0)// Šest užitečných bytů

{

MessageBox(NULL, "Could not read info header", "ERROR", MB_OK);

if(fTGA != NULL)

{

fclose(fTGA);

}

return false;

}

Máme dost informací pro určení výšky, šířky a barevné hloubky obrázku. Uložíme je do obou struktur - textury i obrázku.

texture->width = tga.header[1] * 256 + tga.header[0];// Šířka

texture->height = tga.header[3] * 256 + tga.header[2];// Výška

texture->bpp = tga.header[4];// Barevná hloubka v bitech

// Kopírování dat do struktury obrázku

tga.Width = texture->width;

tga.Height = texture->height;

tga.Bpp = texture->bpp;

Otestujeme, jestli má obrázek alespoň jeden pixel a jestli je barevná hloubka 24 nebo 32 bitů.

// Platné hodnoty?

if((texture->width <= 0) || (texture->height <= 0) || ((texture->bpp != 24) && (texture->bpp != 32)))

{

MessageBox(NULL, "Invalid texture information", "ERROR", MB_OK);

if(fTGA != NULL)

{

fclose(fTGA);

}

return false;

}

Nyní nastavíme typ obrázku. V případě 24 bitů je jím GL_RGB, u 32 bitů má obrázek i alfa kanál, takže použijeme GL_RGBA.

if(texture->bpp == 24)// 24 bitový obrázek?

{

texture->type = GL_RGB;

}

else// 32 bitový obrázek

{

texture->type = GL_RGBA;

}

Spočítáme barevnou hloubku v BYTECH a celkovou velikost paměti potřebnou pro data. Vzápětí se ji pokusíme alokovat.

tga.bytesPerPixel = (tga.Bpp / 8);// BYTY na pixel

tga.imageSize = (tga.bytesPerPixel * tga.Width * tga.Height);// Velikost paměti

texture->imageData = (GLubyte *)malloc(tga.imageSize);// Alokace paměti pro data

if(texture->imageData == NULL)// Alokace neúspěšná

{

MessageBox(NULL, "Could not allocate memory for image", "ERROR", MB_OK);

fclose(fTGA);

return false;

}

Pokud se podařila alokace paměti, nahrajeme do ní data obrázku.

// Pokusí se nahrát data obrázku

if(fread(texture->imageData, 1, tga.imageSize, fTGA) != tga.imageSize)

{

MessageBox(NULL, "Could not read image data", "ERROR", MB_OK);

if(texture->imageData != NULL)

{

free(texture->imageData);// Uvolnění paměti

}

fclose(fTGA);

return false;

}

Formát TGA se od formátu OpenGL liší tím, že má v pixelech přehozené R a B složky barvy (BGR místo RGB). Musíme tedy zaměnit první a třetí byte v každém pixelu. Abychom tuto operace urychlili, provedeme tři binární operace XOR. Výsledek je stejný jako při použití pomocné proměnné.

// Převod BGR na RGB

for(GLuint cswap = 0; cswap < (int)tga.imageSize; cswap += tga.bytesPerPixel)

{

texture->imageData[cswap] ^= texture->imageData[cswap+2] ^=

texture->imageData[cswap] ^= texture->imageData[cswap+2];

}

Obrázek jsme úspěšně nahráli, takže zavřeme soubor a vrácením true oznámíme úspěch.

fclose(fTGA);// Zavření souboru

return true;// Úspěch

// Paměť dat obrázku se uvolňuje až po vytvoření textury

}

Nyní přistoupíme k nahrávání obrázku komprimovaného metodou RLE (RunLength Encoded). Začátek je stejný jako u nekomprimovaného obrázku - načteme výšku, šířku a barevnou hloubku, ošetříme neplatné hodnoty a spočítáme velikost potřebné paměti, kterou opět alokujeme. Všimněte si, že velikost požadované paměti je taková, aby do ní mohla být uložena data PO DEKOMPRIMOVÁNÍ, ne před dekomprimováním.

bool LoadCompressedTGA(Texture* texture, char* filename, FILE* fTGA)// Nahraje komprimovaný obrázek

{

if(fread(tga.header, sizeof(tga.header), 1, fTGA) == 0)// Šest užitečných bytů

{

MessageBox(NULL, "Could not read info header", "ERROR", MB_OK);

if(fTGA != NULL)

{

fclose(fTGA);

}

return false;

}

texture->width = tga.header[1] * 256 + tga.header[0];// Šířka

texture->height = tga.header[3] * 256 + tga.header[2];// Výška

texture->bpp = tga.header[4];// Barevná hloubka v bitech

// Kopírování dat do struktury obrázku

tga.Width = texture->width;

tga.Height = texture->height;

tga.Bpp = texture->bpp;

// Platné hodnoty?

if((texture->width <= 0) || (texture->height <= 0) || ((texture->bpp != 24) && (texture->bpp != 32)))

{

MessageBox(NULL, "Invalid texture information", "ERROR", MB_OK);

if(fTGA != NULL)

{

fclose(fTGA);

}

return false;

}

if(texture->bpp == 24)// 24 bitový obrázek?

{

texture->type = GL_RGB;

}

else// 32 bitový obrázek

{

texture->type = GL_RGBA;

}

tga.bytesPerPixel = (tga.Bpp / 8);// BYTY na pixel

tga.imageSize = (tga.bytesPerPixel * tga.Width * tga.Height);// Velikost paměti

texture->imageData = (GLubyte *)malloc(tga.imageSize);// Alokace paměti pro data (po dekomprimování)

if(texture->imageData == NULL)// Alokace neúspěšná

{

MessageBox(NULL, "Could not allocate memory for image", "ERROR", MB_OK);

fclose(fTGA);

return false;

}

Dále potřebujeme zjistit přesný počet pixelů, ze kterých je obrázek složen. Jednoduše vynásobíme výšku obrázku se šířkou. Také musíme znát, na kterém pixelu se právě nacházíme a kam do paměti zapisujeme.

GLuint pixelcount = tga.Height * tga.Width;// Počet pixelů

GLuint currentpixel = 0;// Aktuální načítaný pixel

GLuint currentbyte = 0;// Aktuální načítaný byte

Alokujeme pomocné pole tří nebo čtyř bytů (podle barevné hloubky) k uložení jednoho pixelu. Překl.: Měla by se testovat správnost alokace paměti!

GLubyte* colorbuffer = (GLubyte *)malloc(tga.bytesPerPixel);// Paměť pro jeden pixel

// Překl.: Test úspěšnosti alokace paměti !!!

// if(colorbuffer == NULL)// Alokace neúspěšná

// {

// MessageBox(NULL, "Could not allocate memory for color buffer", "ERROR", MB_OK);

// fclose(fTGA);

// return false;

// }

V hlavním cyklu deklarujeme proměnnou k uložení bytu hlavičky, který definuje, jestli je následující sekce obrázku ve formátu RAW nebo RLE a jak dlouhá je. Pokud je byte hlavičky menší nebo roven 127, jedná se o RAW hlavičku. Hodnota v ní uložená, určuje počet pixelů mínus jedna, které vzápětí načteme a zkopírujeme do paměti. Po těchto pixelech se v souboru vyskytuje další byte hlavičky. Pokud je byte hlavičky větší než 127, představuje toto číslo (zmenšené o 127), kolikrát se má následující pixel v dekomprimovaném obrázku opakovat. Hned po něm se bude vyskytovat další hlavičkový byte. Načteme hodnoty tohoto pixelu a zkopírujeme ho do imageData tolikrát, kolikrát potřebujeme.

Podstatu komprese RLE tedy už znáte, podívejme se na kód. Jak jsem již zmínil, založíme cyklus přes celý soubor a pokusíme se načíst byte první hlavičky.

do// Prochází celý soubor

{

GLubyte chunkheader = 0;// Byte hlavičky

if(fread(&chunkheader, sizeof(GLubyte), 1, fTGA) == 0)// Načte byte hlavičky

{

MessageBox(NULL, "Could not read RLE header", "ERROR", MB_OK);

if(fTGA != NULL)

{

fclose(fTGA);

}

if(texture->imageData != NULL)

{

free(texture->imageData);

}

// Překl.: Uvolnění dynamické paměti !!!

// if(colorbuffer != NULL)

// {

// free(colorbuffer);

// }

return false;

}

Pokud se jedná o RAW hlavičku, přičteme k bytu jedničku, abychom získali počet pixelů následujících po hlavičce. Potom založíme další cyklus, který načítá všechny požadovaného pixely do pomocného pole colorbuffer a vzápětí je ve správném formátu ukládá do imageData.

if(chunkheader < 128)// RAW část obrázku

{

chunkheader++;// Počet pixelů v sekci před výskytem dalšího bytu hlavičky

for(short counter = 0; counter < chunkheader; counter++)// Jednotlivé pixely

{

// Načítání po jednom pixelu

if(fread(colorbuffer, 1, tga.bytesPerPixel, fTGA) != tga.bytesPerPixel)

{

MessageBox(NULL, "Could not read image data", "ERROR", MB_OK);

if(fTGA != NULL)

{

fclose(fTGA);

}

if(colorbuffer != NULL)

{

free(colorbuffer);

}

if(texture->imageData != NULL)

{

free(texture->imageData);

}

return false;

}

Při kopírování do imageData prohodíme pořadí bytů z formátu BGR na RGB. Pokud je v obrázku i alfa kanál, zkopírujeme i čtvrtý byte. Abychom se přesunuli na další pixel popř. byte hlavičky, zvětšíme aktuální byte o barevnou hloubku (+3 nebo +4). Inkrementujeme také počet načtených pixelů.

// Zápis do paměti, prohodí R a B složku barvy

texture->imageData[currentbyte] = colorbuffer[2];

texture->imageData[currentbyte + 1] = colorbuffer[1];

texture->imageData[currentbyte + 2] = colorbuffer[0];

if(tga.bytesPerPixel == 4)// 32 bitový obrázek?

{

texture->imageData[currentbyte + 3] = colorbuffer[3];// Kopírování alfy

}

currentbyte += tga.bytesPerPixel;// Aktualizuje byte

currentpixel++;// Přesun na další pixel

Zjistíme, jestli je pořadová číslo aktuálního pixelu větší než celkový počet pixelů. Pokud ano, je soubor obrázku poškozen nebo je v něm někde chyba. Jak jsme na to přišli? Máme načítat další pixel, ale defakto je už máme všechny načtené, protože aktuální hodnota je větší než maximální. Nestačila by alokovaná paměť pro dekomprimovanou verzi obrázku. Tuto skutečnost musíme každopádně ošetřit.

if(currentpixel > pixelcount)// Jsme za hranicí obrázku?

{

MessageBox(NULL, "Too many pixels read", "ERROR", NULL);

if(fTGA != NULL)

{

fclose(fTGA);

}

if(colorbuffer != NULL)

{

free(colorbuffer);

}

if(texture->imageData != NULL)

{

free(texture->imageData);

}

return false;

}

}

}

Vyřešili jsme část RAW, nyní implementujeme sekci RLE. Ze všeho nejdříve od bytu hlavičky odečteme číslo 127, abychom získali kolikrát se má následující pixel opakovat.

else// RLE část obrázku

{

chunkheader -= 127;// Počet pixelů v sekci

Načteme jeden pixel po hlavičce a potom ho požadovaně-krát vložíme do imageData. Opět zaměňujeme formát BGR za RGB. Stejně jako minule inkrementujeme aktuální byte i pixel a ošetřujeme přetečení.

if(fread(colorbuffer, 1, tga.bytesPerPixel, fTGA) != tga.bytesPerPixel)// Načte jeden pixel

{

MessageBox(NULL, "Could not read from file", "ERROR", MB_OK);

if(fTGA != NULL)

{

fclose(fTGA);

}

if(colorbuffer != NULL)

{

free(colorbuffer);

}

if(texture->imageData != NULL)

{

free(texture->imageData);

}

return false;

}

for(short counter = 0; counter < chunkheader; counter++)// Kopírování pixelu

{

// Zápis do paměti, prohodí R a B složku barvy

texture->imageData[currentbyte] = colorbuffer[2];

texture->imageData[currentbyte + 1] = colorbuffer[1];

texture->imageData[currentbyte + 2] = colorbuffer[0];

if(tga.bytesPerPixel == 4)// 32 bitový obrázek?

{

texture->imageData[currentbyte + 3] = colorbuffer[3];// Kopírování alfy

}

currentbyte += tga.bytesPerPixel;// Aktualizuje byte

currentpixel++;// Přesun na další pixel

if(currentpixel > pixelcount)// Jsme za hranicí obrázku?

{

MessageBox(NULL, "Too many pixels read", "ERROR", NULL);

if(fTGA != NULL)

{

fclose(fTGA);

}

if(colorbuffer != NULL)

{

free(colorbuffer);

}

if(texture->imageData != NULL)

{

free(texture->imageData);

}

return false;

}

}

}

Hlavní cyklus opakujeme tak dlouho, dokud v souboru zbývají nenačtené pixely. Po konci loadingu soubor zavřeme a vrácením true indikujeme úspěch.

} while(currentpixel < pixelcount);// Pokračuj dokud zbývají pixely

// Překl.: Uvolnění dynamické paměti !!!

// if(colorbuffer != NULL)

// {

// free(colorbuffer);

// }

fclose(fTGA);// Zavření souboru

return true;// Úspěch

// Paměť dat obrázku se uvolňuje až po vytvoření textury

}

Nyní jsou data obrázku připravena pro vytvoření textury a to už jistě zvládnete sami. V tomto tutoriálu nám šlo především o nahrávání TGA obrázků. Ukázkové demo bylo vytvořeno jen proto, abyste viděli, že kód opravdu funguje.

A jak je to s úspěšností komprimace metody RLE? Je jasné, že nejmenší paměť bude zabírat obrázek s rozsáhlými plochami stejných pixelů (na řádcích). Pokud chcete čísla, tak si vezmeme na pomoc obrázky použité v tomto demu: oba jsou 128x128 pixelů veliké, nekomprimovaný zabírá na disku 48,0 kB a komprimovaný pouze 5,29 kB. Na obou je sice něco jiného, ale devítinásobné zmenšení velikosti mluví za vše.

napsal: Evan Pipho - Terminate <terminate (zavináč) gdnmail.net>
přeložil: Michal Turek - Woq <WOQ (zavináč) seznam.cz>

Zdrojové kódy

Lekce 33

<<< Lekce 32 | Lekce 34 >>>