czwartek, 16 lipca 2009

Składowanie obrazów w bazie danych

0 komentarze
Istnieje wiele sytuacji kiedy zapis obrazka w bazie danych może okazać się najlepszym rozwiązaniem i może znacznie uprościć logikę aplikacji. Warto z tego podejścia korzystać kiedy algorytmy przetwarzania obrazu trwają dosyć długo i nie byłoby wskazane ich powtarzanie za każdym razem przy starcie programu. Innym przykładem może być potrzeba zapisania korekt lub obrysów naniesionych przez użytkownika na obrazie. Jest to proces czasochłonny i nikt nie chce robić tego dwa razy.

Oczywiście obrazek zawsze można umieścić w systemie plików, a w samej bazie zapisać ścieżkę dostępu. Jednak w ten sposób musimy dbać o dwie bazy: relacyjną i plikową. Może to powodować niepotrzebne komplikacje i z punktu widzenia szybkości działania programu jest rozwiązaniem wolniejszym.

W tym wpisie zostanie pokazane jak przygotować małą, graficzną bazę danych w oparciu o CImg i Sqlite.

Biblioteka Sqlite świetnie nadaje się do takich zastosowań, ponieważ działa w obrębie tworzonego programu - baza danych jest wbudowywana w aplikację. Jest to zupełnie inne podejście, aniżeli to spotykane w dużych, komercyjnych bazach danych działających w trybie klient-serwer. W tym wypadku mamy bazę plikową, która jest zalecana do pracy z jednym klientem.

Aby rozpocząć pracę z Sqlite należy zainstalować następujące pakiety (przykład dla Ubuntu):

sudo apt-get install sqlite3 libsqlite3-0 libsqlite3-dev

Następnie utworzymy testową bazę danych, a w niej jedną tabelę do gromadzenia obrazów. Będą one zapisywane w polu photo typu blob.

sqlite3 image_db.sqlite3
create table photos(id integer primary key autoincrement, photo blob, filename char, dimx integer, dimy integer, dimz integer, dimv integer);
.q

Poniżej został zaprezentowany przykładowy program pozwalający na dodawanie obrazów do bazy danych oraz ich odczyt. Analiza kodu oraz komentarzy w nim zawartych powinna umożliwić szybkie zrozumienie jego logiki działania.

#include <stdio.h>
#include <stdlib.h>
#include <sqlite3.h>

#include "CImg.h"

using namespace std;
using namespace cimg_library;

int main(int argc, char **argv){
sqlite3 *db;
char *zErrMsg = 0;
int rc;
int dx,dy,dz,dv;
int rownum=0, num_bytes;
const unsigned char *filename;
const unsigned char *data_buffer;

//Parametry wejściowe
const char* file_i  = cimg_option("-i", (char*)0, "Input image");
const char* db_name = cimg_option("-d", (char*)0, "Database file");
const int option    = cimg_option("-o", 1,"Option (1-show, 2-add)");

//Sprawdzamy poprawność parametrów wejściowych
if (option != 1 && option != 2)
  throw CImgException("Please specify correct option (1-show, 2-add)");

if (db_name==(char*)0)
  throw CImgException("Please specify database file (option -d)");

if (option == 2 && file_i==(char*)0) {
  throw CImgException("Please specify image file name (option -i)");
  exit(1);
}

//Otwarcie bazy danych
rc = sqlite3_open(db_name, &db);
if( rc ){
  fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
  sqlite3_close(db);
  exit(1);
}

//Definicja zapytań
sqlite3_stmt *insert_stmt, *select_stmt;
char insert_str[] = "insert into photos(photo,filename,dimx,dimy,dimz,dimv) values (?,?,?,?,?,?)";
char select_str[] = "select id, photo,filename,dimx,dimy,dimz,dimv from photos";
char begin_str[]  = "begin transaction";
char commit_str[] = "commit transaction";

//Zapis do bazy
if (option == 2) {
  //Odczyt obrazka z pliku
  CImg<unsigned char> image(file_i);

  //Poczatek tranzakcji
  sqlite3_exec   (db, begin_str, NULL, NULL, NULL);
  sqlite3_prepare(db, insert_str, -1, &insert_stmt, NULL);

  //Wstawienie danych do tabeli
  sqlite3_bind_blob(insert_stmt, 1, image.ptr(), image.size()  * sizeof(unsigned char), SQLITE_STATIC);
  sqlite3_bind_text(insert_stmt, 2, file_i,      strlen(file_i)* sizeof(unsigned char), SQLITE_STATIC);
  sqlite3_bind_int (insert_stmt, 3, image.dimx());
  sqlite3_bind_int (insert_stmt, 4, image.dimy());
  sqlite3_bind_int (insert_stmt, 5, image.dimz());
  sqlite3_bind_int (insert_stmt, 6, image.dimv());
  sqlite3_step (insert_stmt);
  sqlite3_reset(insert_stmt);

  //Zakończenie tranzakcji
  sqlite3_exec(db, commit_str, NULL, NULL, NULL);
}
else {  //option==1
  //Odczyt danych z tabeli
  sqlite3_prepare(db, select_str,-1, &select_stmt, NULL);

  while (sqlite3_step(select_stmt) == SQLITE_ROW) {
    //odczytujemy nazwe pliku
    filename = sqlite3_column_text(select_stmt, 2);
    //odczytujemy rozmiary obrazu
    dx = sqlite3_column_int(select_stmt, 3);
    dy = sqlite3_column_int(select_stmt, 4);
    dz = sqlite3_column_int(select_stmt, 5);
    dv = sqlite3_column_int(select_stmt, 6);
    //pobieramy obraz
    int num_bytes = sqlite3_column_bytes(select_stmt,1);
    num_bytes = num_bytes / sizeof(unsigned char);
    data_buffer = new unsigned char[num_bytes];
    data_buffer = (const unsigned char*)sqlite3_column_blob(select_stmt, 1);

    printf("%s (%d,%d,%d,%d)\n",filename,dx,dy,dz,dv);

    //tworzymy i wyświetlamy obraz
    CImg<unsigned char> tmp(data_buffer,dx,dy,dz,dv);
    CImgDisplay main_img(tmp, "Obraz wejsciowy");
    while (!main_img.is_closed);
  }
}

sqlite3_close(db);
return 0;
}

Podczas kompilacji poza standardowymi bibliotekami wymaganymi przez CImg dołączamy również bibliotekę sqlite.

g++ imagedb1.cpp -o imagedb1 -lX11 -lpthread -lsqlite3

Przykładowe wywołanie programu dodające 2 obrazki do bazy:

./imagedb1 -d image_db.sqlite3 -o 2 -i input1.bmp
./imagedb1 -d image_db.sqlite3 -o 2 -i input2.bmp

Wyświetlenie obrazów z bazy:

./imagedb1 -d image_db.sqlite3

Analizując wyżej przedstawiony przykład łatwo można dostrzec zalety korzystania z baz danych w aplikacjach służących do przetwarzania obrazu. Najważniejszą z nich jest możliwość zapisywania stanu aplikacji i kontynuowania pracy po jego kolejnym uruchomieniu.