Reprezentarea imaginii Folosim pentru simplitate imagini în format bitmap (.bmp) care reprezintă independent culoarea fiecărui pixel din imagine. Varietatea paletei de culori e dată de numărul de biți utilizați. Vom lucra cu imagini care folosesc 24 de biți pe pixel. Culorile sunt codificate în format RGB: câte un octet (8 biți) pentru fiecare din cele trei culori de bază: roșu, verde și albastru. Valoarea maximă, 0xFF (255) reprezintă intensitate maximă, iar valoarea 0 intensitate zero. Toate trei componentele la maxim dau ca rezultat alb; toate trei valorile zero înseamnă negru. Valoarea 0xFF pentru R și zero pentru celelalte reprezintă roșu aprins; valoarea 0xFF pentru roșu și verde, împreună cu 0 pentru albastru dă galben, etc.
Structura unui fișier imagine Un fișier cu o imagine bitmap conține întâi un antet cu informații despre imagine (dimensiune, metodă de codificare, paletă de culori, etc.). Pentru formatul bitmap cu care lucrăm, antetul are 54 de octeți. După antet urmează imaginea propriu-zisă, reprezentată în acest caz cu câte 3 octeți pe pixel.
Citirea și scrierea imaginilor
Pentru simplitate, programele pe care le scriem vor citi și scrie fișierele de la intrarea standard respectiv la ieșirea standard. Aceasta ne va permite să scriem în continuare programele cu funcțiile simple și cunoscute getchar și putchar, care citesc, respectiv scriu câte un octet (un caracter) de la intrarea / la ieșirea standard. Numele fișierlor imagine cu care lucrăm le indicăm din linia de comandă:
./programexecutabil < intrare.bmp > iesire.bmp
Structura programului Întrucât nu vom modifica dimensiunile imaginii, antetul va fi copiat neschimbat în imaginea de ieșire. Programul nostru va avea deci structura:
// copiaza 54 de caractere(octeti) cu getchar/putchar de la intrare la iesire // cat timp nu s-a terminat intrarea // citeste cate un octet (sau cate trei pentru un pixel) // modifica octetul conform prelucrarii dorite // scrie octetul la iesireO prelucrare simplă Pentru început, vom vedea ce efect are modificarea componentelor de culoare din imagine. Să presupunem că un pixel are componenta de roșu cu valoarea 173 (cca 2/3 din intensitate între 0 și 255). În binar, 173 e 10101101, sau 0xAD în hexazecimal. Să presupunem acum că punem pe zero cei 4 biți mai puțin semnificativi, și valoarea devine 10100000, adică 0xA0 sau 160, și aplicăm aceeași prelucrare tuturor pixelilor, pentru toate cele 3 culori. Aceasta are două efecte observabile: imaginea devine mai întunecată pe ansamblu (intensitatea fiecărei componente fiind mai mică). În plus, calitatea imaginii scade, apar pete de culoare mai uniformă, deoarece toți pixelii care aveau înainte valori în intervalul [16*n, 16*n+15] (de exemplu [160, 175]) vor avea acum valori egale (160).
De implementat (partea 1) Codificați în program prelucrarea indicată (resetând cei mai puțini semnificativi 4 biți din fiecare componentă de culoare), și observați efectul. Variați apoi numărul de biți afectați (5, 3, 2, ...). Implementați varianta în care setați biții respectivi pe 1. Efectul de pete de culoare va fi același, dar imaginea va deveni mai luminoasă.
De implementat (partea 2) Veți observa că dacă nu modificați decât bitul cel mai puțin semnificativ (sau ultimii doi), efectele sunt practic nedetectabile cu ochiul liber. Vom folosi această observație pentru a încorpora astfel în imagine date ascunse fără ca altcineva să își dea seama de modificare (un procedeu numit steganografie).
Informația transmisă va fi un șir de numere generate aleator, cu funcția standard rand (declarată în stdlib.h). Veți scrie un program care codifică un șir de numere aleatoare într-o imagine, și apoi un alt program care le extrage și le afișează.
Pentru o structură cât mai simplă a programului e util să privim informația de codificat ca un șir de biți: pentru fiecare componentă de culoare a fiecărui pixel vom solicita valoarea bitului pe care dorim să-l inserăm pe ultima poziție. Vom scrie pentru aceasta o funcție genbit care la fiecare apel va returna câte un bit (valoarea 1 sau 0) din informația pe care vrem s-o codificăm (șirul de numere aleatoare).
Funcția va avea un contor care îi va spune câti biți mai are disponibili (de furnizat) din număr. La fiecare apel, contorul va fi decrementat; când ajunge la 0, funcția va genera un nou număr aleator.
În limbajul C, variabilele locale sunt distruse la revenirea dintr-un apel de funcție (au durată de memorare "automată"), deci nu se păstrează valoarea lor. Excepție fac variabilele declarate cu specificatorul static: valoarea acestora se păstrează între două apeluri. Atât contorul cât și numărul aleator generat vor fi declarate cu durată de memorare statică. Funcția noastră va avea deci structura:
int genbit(void) // returneaza urmatorul bit de codificat { static int cnt = 0; // numarul de biti disponibil, initial 0 static int nr; // numarul aleator generat if (cnt == 0) { nr = rand(); // genereaza un nou numar aleator cnt = 8 * sizeof(int); // cati biti are numarul generat fprintf(stderr, "%d ", nr); // tipareste numarul si un spatiu } // extrage un bit din numar (incepand de la un capat) // deplaseaza numarul cu o pozitie (pregateste bitul urmator) // decrementeaza contorul // returneaza valoarea bitului extras (1 sau 0) }Odată ce un numar e generat, funcția îl tipăreste pentru a putea sa comparăm șirul de numere cu cel care va fi extras din imagine. Folosim functia fprintf, care functioneaza la fel ca printf dar la care ii specificam in plus ca prim argument fișierul în care să facă tipărirea. Folosim în acest scop fișierul stderr, destinat în mod obișnuit mesajelor (de ex. de eroare) care trebuie să apară pe ecran, chiar dacă ieșirea programului e redirectată altundeva (ca în acest caz, unde la ieșire scriem imaginea).
Cu această funcție, programul păstrează mai departe aceeași structură simplă de mai sus, în care citește succesiv octeți imaginii de la intrare, îi modifică (cu bitul obținut cu genbit) și îi scrie la ieșire.
Scriem un al doilea program care extrage informația codificată dintr-o imagine. Pentru aceasta, concepem o funcție pereche putbit care primește la fiecare apel câte un bit, îi asamblează într-un număr (în același sens în care au fost obținuți), iar când numărul e complet, îl tipărește la ieșire:
void putbit(int bit) { static int cnt = 8 * sizeof(int); // cati biti mai trebuie static int nr = 0; // numarul care e asamblat // deplaseaza numarul si insereaza bitul // decrementeaza contorul // daca contorul e zero: // tipareste numarul obtinut // reinitializeaza contorul si numarul }Puteți implementa o variantă în care programul de codificare folosește 0 ca fanion pentru a indica sfârșitul șirului de numere, iar cel de decodificare îl detectează și oprește afișarea și codificarea. Alternativ, puteți structura programul de decodificare avănd ca ciclu principal asamblarea informatiei (ca în putbit), și apelând citirea de la intrare pentru a obține fiecare bit.
Puteți codifica și o imagine de aceleași dimensiuni în ultimii 2 sau 3 biți inferiori ai fiecărei componente de culoare din altă imagine. Imaginea codificată va avea un număr redus de nuanțe (8 nuanțe pentru fiecare componentă de culoare, dacă folosim 3 biți). Recreem imaginea ascunsa plasând biții codificați (de ex. 101) pe pozițiile superioare (1010000). Un exemplu e dat aici.
Puteți efectua și alte prelucrări care nu țin de codificare: să inversați intensitatea culorilor din imagine (negativ), să o transformați în nuanțe de gri (folosind uniform media aritmetică a celor trei componente), să modificați luminozitatea, să obțineți diverse efecte de culoare, etc.