ASCII art
Képnézegetés parancssoron
 Összes Adatszerkezetek Fájl Függvények Változók Típusdefiníciók Enumeráció-értékek Oldal
ASCII art

Tartalomjegyzék

Bevezetés

Egy ASCII art programot írunk, mely bittérképes (bitmap) képeket a parancssoron ábrázol ASCII karakterek segítségével:

A program kimenete

Az alkalmazás segítségével a fájlkezelést, a dinamikus tömböket és a struktúrákat gyakoroljuk.

Specifikáció

A program parancssori alkalmazás, mely bekéri egy PPM formátumú bittérképes képfájl nevét, és a standard outputra rajzolja ki azt ASCII karakterekkel. A bemenet formátumának azért a PPM-et választjuk, mert az az egyik legegyszerűbben kezelhető tömörítetlen bittérképes formátum.

Megjegyzés
Ezzel nem csorbítjuk a program általánosságát, mert egyszerű parancssoros konverter programokkal, mint pl. az imagemagick rengeteg különböző formátumot PPM-mé tudunk alakítani. Így legrosszabb esetben a program használata kiegészül az alábbi konverzióval
convert.exe batman.jpeg batman.ppm
ahol a convert.exe az imagemagick futtatható állománya, mely az imagemagick telepítése után a parancssorban elérhető.

Algoritmus és adatszerkezetek

Az algoritmus egyszerű:

Beolvassuk a bitmapet a képfájlból a memóriába
Kirajzoljuk a standard outputra

A bitmap szerkezete

A beolvasáshoz természetesen pontosan ismernünk kell a PPM fájl szerkezetét. A PPM fájl egy rövid szöveges fejlécből, majd a kép pixeleit leíró bináris adatsorból áll. A szöveges fejléc pl. az alábbi lehet:

P6
640 480
255

A PPM formátum színes képeket is képes tárolni, melyeknél egy pixelt nem írhatunk le egyetlen fényességértékkel, hanem külön meg kell adnunk annak vörös (R), zöld (G) és kék (B) színösszetevőjét. Minden egyes színösszetevőt a 0-255 tartományon mozgó világosságadattal jellemzünk, ahol a 0 a legsötétebb (fekete), 255 pedig a legvilágosabb árnyalat. Példánk bináris adatsora tehát összesen 3*640*480 bájtot tartalmaz a következő rendszerben:

+--------------+--------------+--------------+-----+----------------------+
| p[0] (R,G,B) | p[1] (R,G,B) | p[2] (R,G,B) | ... | p[640*480-1] (R,G,B) |
+--------------+--------------+--------------+-----+----------------------+
  3 bájt         3 bájt

A pixelek tárolása sorfolytonos, vagyis a p[0]--p[639] pixelek a kép legfelső sorának pixelei, a p[640]--p[1279] pixelek a második sort adják meg és így tovább.

A bitmap C-adatszerkezete

A bitmap tárolására az alábbi típusokat vezetjük be:

typedef unsigned char Byte;
typedef struct {
Byte r;
Byte g;
Byte b;
typedef struct {
unsigned height;
unsigned width;
Pixel *pixels;

A beolvasás algoritmusa

A fentiek értelmében a beolvasás algoritmusa így bontható tovább:

Megnyitjuk a képfájlt
Kiolvassuk a szöveges PPM-fejlécből a méreteket
memóriát foglalunk a pixeleknek egy dinamikus tömbben
Beolvassuk a pixeleket a tömbbe
Bezárjuk a képfájlt

A képkezelő függvények

A beolvasó függvényünk az image_read függvény, mely paraméterként a fájl nevét kapja, kimenete pedig egy pointer egy újonnan létrehozott Image struktúrára. Hibakezelés nélkül a függvény az alábbi:

Image *image_read(char *fname)
{
FILE *fp;
Image *image;
unsigned depth;
image = (Image *)malloc(sizeof(Image));
fp = fopen(fname, "rb");
fscanf(fp, "P6\n%u%u%u%*c", &image->width, &image->height, &depth);
image->pixels = (Pixel *)malloc(image->height*image->width*sizeof(Pixel));
fread(image->pixels, sizeof(Pixel), image->height*image->width, fp);
fclose(fp);
return image;
}

A függvény az fscanf függvénnyel olvassa ki a szöveges fejlécet, majd fread-del a bináris adatokat.

Megjegyzés
(18+) Ha feltűnt, és zavar, hogy ugyanazon a fájlon használjuk az fscanf-et és az fread-et, akkor olvasd el ezt.

Vegyük észre, hogy a függvény nemcsak a pixelek tömbjét foglalja dinamikusan, hanem magát az Image struktúrát is, és nem a struktúrát adja vissza, hanem a dinamikusan foglalt struktúra címét. Ez a tervezési elv nagyon hasonlít ahhoz, ahogy a C könyvtári függvényei pl. a fájlokat kezelik. Az fopen függvény is egy mutatót ad vissza az újonnan foglalt FILE struktúrára, és az fclose függvény szabadítja fel a dinamikusan foglalt adatokat. Nekünk is lesz egy image_close függvényünk, melynek feladata a képstruktúra és a benne tárolt pixeladatok felszabadítása:

void image_close(Image *image)
{
free(image->pixels);
free(image);
}

További egyszerűsítésként bevezetünk egy get_pixel függvényt, mely paraméterként pointert kap az Image struktúrára, továbbá egy pixel két koordinátáját, majd visszaadja a kép adott pixelét:

Pixel get_pixel(Image *image, int h, int w)
{
int idx = h*image->width+w;
return image->pixels[idx];
}

Ez a függvény nagyon hasznos, mert elfedi a programozótól a sorfolytonos tárolás indexelési problémáját. A továbbiakban mindig e függvényen keresztül kérjük le a pixeladatokat, és nem kell többet sorfolytonos indexpozíciókat számolnunk.

A bitmap kirajzolása

A kirajzolás algoritmusa így bontható tovább:

A képet téglalap alakú blokkokra osztjuk, minden blokkot egyetlen ASCII karakterrel ábrázolunk
MINDEN blokkra fentről lefelé, balról jobbra
    Meghatározzuk a blokk pixeleinek átlagos fényerejét
    Megfelelő fényességű ASCII karaktert írunk ki a kimenetre

Milyen fényesek az ASCII karakterek? A szóköz nyilván 0 fényességű, a @ és a # pedig eléggé kitöltött, tehát világos. Részletesebb információ letölthető az internetről egyszerű táblázatban.

Megjegyzés
A fenti Wikipedia táblázat 3 és 36 közötti fényességértékeket tartalmaz, és nem teljes. Pl. hiányzik belőle az 5 és a 10 is. Vannak benne ugyanakkor ismétlődések (több karakter azonos fényességgel), melyeket kis csalással kihasználhatunk a hiányzó helyek feltöltésére.

Programunkban 36 ASCII karaktert tárolunk az ASCII tömbben, fényesség szerint növekvő sorrendben. Ezek szerint a minimális fényességérték a 0 (szóköz), a maximum pedig a 35 (kukac).

char ASCII[36] = {
' ', '.', '\'', '`', '\'', '-', '^', '\"', '!',
'>', ';', '=', '0', 'i', 'j', 'l', 't', 'c',
'f', 'o', 'a', 'V', '5', 'e', 'b', 'D', 'p',
'g', 'B', 'm', 'H', 'H', 'H', 'M', 'M', '@'
};

A width x height méretű kép blokkora bontásához definiáljuk, hogy a kimenetünk 80 ASCII karakter széles lesz, így egyetlen ASCII karakterhez egy W = width/80 pixel széles blokk tartozik. A blokk pixelben mért magasságát hasraütésszerűen a szélesség kétszeresére (H = 2*W) választjuk, mert karaktereink is magasabbak, mint amilyen szélesek.

Megjegyzés
A pontos arány természetesen a konzolablak betűtípusának beállításain múlik.

A blokkok száma így

A blokkokat bejárjuk egy dupla for ciklussal, és minden blokkon belül annak pixeleit bejárjuk egy dupla for ciklussal, melyben átlagoljuk a pixelek fényességét.

A blokkok pixeleinek átlagos fényességéhez definiálnunk kell egyetlen RGB pixel fényességét. Egy egyszerű definíció szerint átlagoljuk a pixel R G és B színkomponenseinek fényerejét. Ezt a műveletet kiemeltük a pixel_brightness függvénybe, mely paraméterként egy Pixel adatot kap, és egy Byte adatban adja meg annak fényerejét:

{
return p.r/3. + p.g/3. + p.b/3.;
}

Figyeljük meg, hogy a függvény a Byte koordinátákat double értékekkel osztja, majd a double adatokat adja össze, az eredményt pedig ismét Byte-tá alakítja, vagyis kerekíti. Ezzel elkerüljük a túlcsordulást és az egész osztás kerekítési hibáját is.

Mivel a fenti definíciókkal egy blokk átlagos fényességét is 0 és 255 közötti értékként kapjuk meg, ezt át kell skáláznunk a 0-35 intervallumra a

int c = brightness / 256. * 36.;

művelettel, majd az ASCII tömböt a c értékkel indexelhetjük.

A kirajzolást végző függvény az image_to_ascii:

void image_to_ascii(Image *image)
{
int W = image->width/ASCII_WIDTH + 1;
int H = W * 2;
int h, w;
for (h = 0; h < image->height/H; ++h) {
for (w = 0; w < image->width/W; ++w) {
int i, j, b = 0;
for (i = 0; i < H; ++i) {
for (j = 0; j < W; ++j) {
Pixel p = get_pixel(image, h*H+i, w*W+j);
}
}
b *= 36 / 256.0 / (W * H);
printf("%c", ASCII[b]);
}
printf("\n");
}
}

A főprogram

A főprogram feladata a fentiek alapján a következő:

int main(void)
{
char fname[MAX_FNAME];
Image *image; /* A képstruktúrára mutató pointer */
printf("fajlnev: ");
scanf("%s", fname);
image = image_read(fname); /* kép beolvasása */
if (image == NULL)
return 1;
image_to_ascii(image); /* kép feldolgozása */
image_close(image); /* kép felszabadítása */
return 0;
}

Fordítás és futtatás

A teljes program (rendes hibakezeléssel együtt) az ascii.c fájlban található. Letölthető innen.

Fordítás Visual Studióval (VS parancssorból)

cl ascii.c /Feascii_art.exe

gcc-vel (Windows alatt MinGW parancssorból)

gcc ascii.c -o ascii_art.exe

Feladatok