Prowadzący zajęcia projektowe: Rafał Petryniak (strona domowa zajęć projektowych).
Spis treści
Zadaniem projektu który opracowujemy jest przygotowanie biblioteki filtrów do operowania na bitmapach. W przygotowanym programie wykorzystywać będziemy kilka popolularnych filtrów graficznych:negatyw,skala szarości,kontrast,nasycenie,binaryzacja.
Całość operacji będzie odbywała się w przestrrzeni barw RGB. Wersja sekwencyjna programu została napisana w środowisku windows. Następnie zostanie zrównoleglona na potrzeby procesora Cell wykorzystywanego w konsoli PS3. Dzięki specjalniej architekturze procesora Cell możliwe jest zwiększenie wydajności wykonywania poszczególnych operacji przez rozdzielenie zadań na jednostki specjalistyczne SPE. Przyspieszenie obliczeń możliwe jest dzięki wykorzystaniu wektoryzacji, taka operacja pozwala na szybszy przepływ danych.
W naszym projekcie wykorzystywac będziemy tylko jedną konsolę PS3. Poniżej zostaną przedstawionie algorytmy sekwencyjne oraz efekty działania poszczególnych filtrów. W kolejnej części przedstawimy algorytmy sekwencyjne oraz ich implementację.
Algorytm działa na pikselach obrazu zamienionych na macierz dwu wymiarową. Każdy wprowadzony przez Nas obraz zostaje zamieniany na macierz dwu wymiarową co ułatwia późniejsze modyfikacje oraz obliczenia przy wyklorzystywaniu filtrów.
Kod programu dla poszczegolnych filtrów :
Algorytm sekwencyjny dla filtra Negatyw Działanie tego algorytmu polega na zamianie barw obrazku wczytanego z pliku. Od 255 odejmujemy w każdym pikselu wartość poszczególnego koloru.Obraz jest zamieniany na tablicę dwu-wymiarową.Negatyw jest to inaczej odwrócony oryginał.
void Negatyw(Tbmp bmp)
{
for (int i = 0; i < bmp.xsize; i++) {
for (int j = 0; j < bmp.ysize; j++) {
bmp.image[i][j].R = 255 - bmp.image[i][j].R;
bmp.image[i][j].G = 255 - bmp.image[i][j].G;
bmp.image[i][j].B = 255 - bmp.image[i][j].B;
}
}
}Algorytm sekwencyjny dla filtra Binaryzacja Binaryzacja obrazu polega na zaklasyfikowaniu poszczególnych pikseli obrazu jako białych bądź czarnych w zależności, czy ich jasność (średnia składowych barwy) znajduje się poniżej lub powyżej ustalonego progu.Binaryzacja obrazu to jego konwersja do postaci czarno- białej bez kolorów pośrednich.Najprostszą metodą binaryzacji jest progowanie, czyli podanie pewnej wartości progu (ang. threshold) który to bedzie miał za zadanie wyznaczenie granicy pomiędzy obiektami a tłem.
void Binaryzacja(Tbmp bmp,int prog)
{
for (int i = 0; i < bmp.xsize; i++) {
for (int j = 0; j < bmp.ysize; j++) {
if((bmp.image[i][j].R + bmp.image[i][j].G + bmp.image[i][j].B) / 3 < prog) {
bmp.image[i][j].R = 0;
bmp.image[i][j].G = 0;
bmp.image[i][j].B = 0;
}
else {
bmp.image[i][j].R = 255;
bmp.image[i][j].G = 255;
bmp.image[i][j].B = 255;
}
}
}
} Algorytm sekwencyjny dla filtra Natężenie koloru Wykorzytujemy trzy kolory składowe i zwiększamy ich intensywność o określonyc próg.Filtr ten pozwala na swobodne operowanie trzema kolorami składowymi RGB. Poprzez odpowiedni dobór tych trzech barw możliwe jest wyostrzenie odpowiednich kolorów na zdjęciu lub przekolorowanie zdjęcia do zupełnie innych barw.
void NatezenieKoloru(Tbmp bmp,double r,double g,double b)
{
for (int i = 0; i < bmp.xsize; i++) {
for (int j = 0; j < bmp.ysize; j++) {
bmp.image[i][j].R = bmp.image[i][j].R/g/b;
bmp.image[i][j].G = bmp.image[i][j].G/r/b;
bmp.image[i][j].B = bmp.image[i][j].B/r/g;
if (bmp.image[i][j].R>255) {
bmp.image[i][j].R = 255;
}
if (bmp.image[i][j].G>255) {
bmp.image[i][j].G = 255;
}
if (bmp.image[i][j].B>255) {
bmp.image[i][j].B = 255;
}
}
}
}
Algorytm sekwencyjny dla filtra Kontrast Wykorzytujemy trzy kolory składowe i zwiększamy ich jasność o określonyc próg. Algorytm działa podobnie jak w przypadku filtra Natężenie koloru.
void Kontrast(Tbmp bmp,double prog)
{
double x = prog * 1.2725;
double v1 = 255/(255 - 2 * x);
double v2 = (-255 + 2 * x)/255;
for(int i = 0; i < bmp.xsize; i++)
for(int j = 0; j < bmp.ysize; j++) {
int r = bmp.image[i][j].R;
int g = bmp.image[i][j].G;
int b = bmp.image[i][j].B;
if(x > 0) {
r = (r < x) ? 0 : (r > 255 - x) ? 255 : v1 * (r - x);
g = (g < x) ? 0 : (g > 255 - x) ? 255 : v1 * (g - x);
b = (b < x) ? 0 : (b > 255 - x) ? 255 : v1 * (b - x);
}
else
if(x < 0) {
r = (-x + v2) * r;
g = (-x + v2) * g;
b = (-x + v2) * b;
}
r = (r > 255) ? 255 : (r < 0) ? 0: r;
g = (g > 255) ? 255 : (g < 0) ? 0: g;
b = (b > 255) ? 255 : (b < 0) ? 0: b;
bmp.image[i][j].R=r;
bmp.image[i][j].G=g;
bmp.image[i][j].B=b;
}
}Algorytm sekwencyjny dla filtra Skala szarości Wykorzytujemy trzy kolory składowe i obliczamy średnią kolorów składowych w każdym pikselu. Następuje zamiana na uśrednione kolory w nowo utworzonym obrazie będącym macierzą dwu-wymiarową. Podaje się średnią wartość jako wartość kolorów RGB.
void WSkaliSzarosci(Tbmp bmp)
{
for (int i = 0; i < bmp.xsize; i++) {
for (int j = 0; j < bmp.ysize; j++) {
int srednia = (bmp.image[i][j].R+bmp.image[i][j].G+bmp.image[i][j].B)/3;
bmp.image[i][j].R=srednia;
bmp.image[i][j].G=srednia;
bmp.image[i][j].B=srednia;
}
}
}
void Srednia(Tbmp bmp,int pro)
{
int ilosc=(2*pro+1)*(2*pro+1);
Tpixel suma; int pozycja[2];
Tbmp bmp2(bmp.xsize,bmp.ysize);
for (int i = 0; i < bmp.xsize; i++)
for (int j = 0; j < bmp.ysize; j++) {
bmp2.image[i][j].R=bmp.image[i][j].R;
bmp2.image[i][j].G=bmp.image[i][j].G;
bmp2.image[i][j].B=bmp.image[i][j].B;
}
for (int i = 0; i < bmp.xsize; i++)
for (int j = 0; j < bmp.ysize; j++) {
suma.R=0;
suma.G=0,
suma.B=0;
for (int I=-pro; I <= pro; I++)
for (int J=-pro; J <= pro; J++) {
pozycja[0]=i+I;
pozycja[0] = (pozycja[0] > bmp.xsize) ? bmp.xsize : (pozycja[0] < 0) ? 0: pozycja[0];
pozycja[1]=j+J;
pozycja[1] = (pozycja[1] > bmp.ysize) ? bmp.ysize : (pozycja[1] < 0) ? 0: pozycja[1];
suma.R+=bmp2.image[pozycja[0]][pozycja[1]].R;
suma.G+=bmp2.image[pozycja[0]][pozycja[1]].G;
suma.B+=bmp2.image[pozycja[0]][pozycja[1]].B;
}
bmp.image[i][j].R=suma.R/ilosc;
bmp.image[i][j].G=suma.G/ilosc;
bmp.image[i][j].B=suma.B/ilosc;
}
}Przygotowany przez nas algorytm wykorzystywał będzie procesor główny konsoli PlayStation 3 oraz procesory specjalizowane. Dzięki odpowiedniemu - dynamicznemu przydziałowi pracy przy przetwarzaniu obrazu stosując odpowiedni filtr wykorzystać będziemy mogli moc obliczeniową.Algorytm równoległy pozwala na wykonywanie wielu operacji jednocześnie, co znacząco skraca czas wykonywanych obliczeń.
W naszym programie obraz jest dzielony na odpowiednie fragmenty tak aby każdy procesor otrzymał równą paczkę danych. Pozostałe natomiast dane obsługiwane będą przez procesor główny.Procesory Cell wyposażone są w zestawy instrukcji typu SIMD. Więkoszość instrukcji można obsługiwać którąś z reprezentacji danch: 128,2x64,4x32,16x8 bitowych danych typu char.
Kod obliczający ilość elementów przypadających na SPU:
num_elements_per_spe =
((int)(table_size/num_spe_threads/CHUNK_SIZE))*CHUNK_SIZE;Poniżej prezentujemy zrównoleglone wersje filtrów.
Ogólna idea zrównoleglenia algorytmu na klastrze opartym o architekture procesorów Cell.
rozsyłanie wiersza głównego macierzy na wszystkie węzły klastra
rozsyłanie odpowienio podzieloną macierz na pozostałe węzły klastra
równoległe wykonywanie obliczeń wykorzystując architekturę procesorów Cell
pobranie wyników z pozostałych węzłów / czynność ta jest powtarzana /
Kod programu dla poszczegolnych filtrów :
Zamiana filtra z wersji sekwencyjnej na równoległą polegała na wektoryzacji zmiennych i funkcji wykorzystywanych w filtrze.
void negatyw(unsigned char* buf, unsigned int size) {
unsigned int i;
vector unsigned char *pixels;
vector unsigned char maxPixel = (vector unsigned char) {255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255};
pixels = (vector unsigned char*) buf;
for (i = 0; i < (size / 16); i++) {
pixels[i] = maxPixel - pixels[i];
}
}Filtry działa dla obrazów w skali szarości. Wszystkie obliczenia są obliczeniami wektorowymi, jako parametr jest podowany próg binaryzacji.
void v_binaryzacja(unsigned char* buf, unsigned int size, unsigned char prog)
{
//tylko dla obrazów w zkali szarosci
unsigned int i;
vector unsigned char *pixels;
pixels = (vector unsigned char*) buf;
vector unsigned char vprog = (vector unsigned char) {prog,prog,prog,1,prog,prog,prog,1,prog,prog,prog,1,prog,prog,prog,1};
vector unsigned char vwhite = (vector unsigned char) {255,255,255,0,255,255,255,0,255,255,255,0,255,255,255,0};
vector unsigned char vblack = (vector unsigned char) {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
vector unsigned char vmax = (vector unsigned char) {255,255,255,0,255,255,255,0,255,255,255,0,255,255,255,0};
vector unsigned char vbool;
for (i = 0; i < (size / 16); i++)
{
vbool = spu_cmpgt(pixels[i],vprog);
pixels[i] = spu_sel(pixels[i], vwhite, vbool);
vbool = vmax-vbool;
pixels[i] = spu_sel(pixels[i], vblack, vbool);
}
}Bardzo prosty filtr oparty na mnożeniu dwóch wektorów. Wektor maski tworzony jest na podstawie podanych parametrów.
void v_natezenie_koloru(unsigned char* buf, unsigned int size,unsigned char sR,unsigned char sG,unsigned char sB)
{
unsigned int i;
unsigned int R = sR/100;
unsigned int G = sG/100;
unsigned int B = sB/100;
vector unsigned char *pixels;
vector unsigned char maskPixel = (vector unsigned char) {B,G,R,1,B,G,R,1, B,G,R,1,B,G,R,1};
pixels = (vector unsigned char*) buf;
for (i = 0; i < (size / 16); i++)
{
pixels[i]=pixels[i]*maskPixel;
}
}Mimo prób filtr zmieniający kolorowy obraz na skale szarości zawiera wiele obliczeń sekwencyjnych.
void v_skala_szarosci(unsigned char* buf, unsigned int size)
{
unsigned int i,j;
unsigned char p[4];
vector unsigned char *pixels;
pixels = (vector unsigned char*) buf;
for (i = 0; i < (size / 16); i++)
{
for (j = 0; j < 4; j++)
{
p[j]=(spu_extract(pixels[i],4*j)+spu_extract(pixels[i],4*j+1)+spu_extract(pixels[i],4*j+2))/3;
}
for (j = 0; j < 4; j++)
{
pixels[i] = spu_insert(p[j],pixels[i],4*j);
pixels[i] = spu_insert(p[j],pixels[i],4*j+1);
pixels[i] = spu_insert(p[j],pixels[i],4*j+2);
}
}
}Obraz wyjściowy jest wynikiem wyliczenia średniej dla dwóch obrazów o tej samej wielkości podawanych na wejściu.
void v_srednia(unsigned char* buf, unsigned char* buf_2,unsigned int size)
{
unsigned int i;
vector unsigned char *pixels;
vector unsigned char *pixels_2;
pixels = (vector unsigned char*) buf;
pixels_2 = (vector unsigned char*) buf_2;
for (i = 0; i < (size / 16); i++)
{
pixels[i]=spu_avg(pixels[i],pixels_2[i]);
}
}Filtr wykorzystuje warunkową funkcję wektorową, zwracając z jednego obrazu piksele, które są uwzględnione w obrazie maski.
void v_maska_img(unsigned char* buf, unsigned char* buf_2,unsigned int size)
{
unsigned int i;
vector unsigned char *pixels;
vector unsigned char *pixels_2;
vector unsigned char vwhite = (vector unsigned char) {255,255,255,0,255,255,255,0,255,255,255,0,255,255,255,0};
pixels = (vector unsigned char*) buf;
pixels_2 = (vector unsigned char*) buf_2;
for (i = 0; i < (size / 16); i++)
{
pixels[i] = spu_sel(pixels[i], vwhite, pixels_2[i]);
}
}W programie zostały przedstawione przez nas wybrane implementacje filtrów. Można stwierdzić że zastosowanie operacji wektorowych zwiększa prędkość wykonywanych obliczeń na klastrze. Dzięki zmiejszeniu ilości kodu uzyskaliśmy zwiększenie w efektywności wykonywanego programu. Zaobserwowaliśmy że dla każdego algorytmu można przeprowadzić proces wektoryzacji na kilka sposobów.
Ogólna idea zrównoleglenia algorytmu na klastrze opartym o architekture procesorów Cell.
rozsyłanie wiersza głównego macierzy na wszystkie węzły klastra
rozsyłanie odpowienio podzieloną macierz na pozostałe węzły klastra
równoległe wykonywanie obliczeń wykorzystując architekturę procesorów Cell
pobranie wyników z pozostałych węzłów / czynność ta jest powtarzana /
Programowanie procesorów Cell jest podobne jak programowanie dla procesorów x86 i x64. Zamiast typów prostych posługujemy się wektorami dla których przewidziane są funkcje zastępujące funkcje operujące na typach prostych. W naszym projekcie przyspieszenie obliczeń jest wyraźnie zauważalne.