Rozdział 4. Kompresja i dekompresja pliku wideo z wykorzystaniem procesorów Cell/B.E.

Sylwester Jarosiński

Paweł Więcek

Tomasz Dragunajtys

Prowadzący zajęcia projektowe: Rafał Petryniak (strona domowa zajęć projektowych).

Spis treści

1. Algorytm sekwencyjny
1.1. Projekt
1.2. Implementacja
2. Algorytm równoległy
2.1. Projekt
2.2. Implementacja

1. Algorytm sekwencyjny

  1. Głownym zadaniem naszego projektu była kompresja i dekompresja pliku video. Kompresję stosuję się przede wszystkim w celu redukcji kosztów przechowywania i transmisji danych.

  2. Za to zadanie odpowiedzialna była biblioteka OpenCV firmy Intel. Używa ona architektury x86. Została napisana w języku C/C++ i jest jedną z najlepszych darmowych bibliotek do przetwarzania obrazów. Posiada ona ogromne możliwości. Jest ona zoptymalizowana pod kątem operacji w czasie rzeczywistym. Posiada własny system zarządzania okienkami.

  3. W projekcie użyliśmy filtru Sobel'a, który wzmacniał bezpośrednio najbliższe otoczenie piksela przetwarzenego. Filtr ten używany jest głównie do detekcji krawędzi. Filtr Sobel'a stosuję się dla krawędzi poziomych i pionowych. Wylicza się dla nich sumę gradientów, a następnie kierunek gradnientu, aby móc określić kierunek krawędzi. W tym celu została użyta fukncja cvFilter2D z pakietu OpenCv.

1.1. Projekt

Rysunek 4.1. Obraz wejściowy

Obraz wejściowy

Rysunek 4.2. Obraz wyjściowy z filtrem

Obraz wyjściowy z filtrem

1.2. Implementacja

#include <cv.h>
#include <cxcore.h>
#include <highgui.h>
#include <stdlib.h>
#include <stdio.h>
#include <math.h>

int main()
{
  CvCapture* capture = cvCreateFileCapture("./video/cat.avi");

  cvNamedWindow("stare okno",0); 1
  cvNamedWindow("nowe okno",0);  2

  cvQueryFrame(capture); 3

  double fps    = cvGetCaptureProperty(capture,CV_CAP_PROP_FPS);  4
  double frameW = cvGetCaptureProperty(capture,CV_CAP_PROP_FRAME_WIDTH);
  double frameH = cvGetCaptureProperty(capture,CV_CAP_PROP_FRAME_HEIGHT);

  int odstep_miedzy_klatkami = 1000 / fps; 5

  CvVideoWriter *writer = 0;
  int isColor = 1;
  int fps_wy     = fps;
  int frameW_wy  = frameW;
  int frameH_wy  = frameH;

  writer = cvCreateVideoWriter("./video/out.avi",CV_FOURCC('P','I','M','1'), fps_wy,cvSize(frameW_wy,frameH_wy),isColor);

  CvMat* maska = cvCreateMat(3,3,CV_32F); 6

  /*  7
  // nasza maska wyglada nastepujaco
  // 0.1 0.1 0.1
  // 0.1 0.2 0.1
  // 0.1 0.1 0.1

  for(int i=0;i<3;i++)
  for(intj=0;j<3;j++)
  cvSet2D(maska,j,i,cvScalarAll(0.1));


  cvSet2D(maska,1,1,cvScalarAll(0.2));
  */

  cvSet2D(maska,2,0,cvScalarAll(1.0)); 8
  cvSet2D(maska,2,1,cvScalarAll(2.0));
  cvSet2D(maska,2,2,cvScalarAll(1.0));
  cvSet2D(maska,1,1,cvScalarAll(0.0));
  cvSet2D(maska,1,1,cvScalarAll(0.0));
  cvSet2D(maska,1,1,cvScalarAll(0.0));
  cvSet2D(maska,0,0,cvScalarAll(-1.0));
  cvSet2D(maska,0,1,cvScalarAll(-2.0));
  cvSet2D(maska,0,2,cvScalarAll(-1.0));

  double all_frames = cvGetCaptureProperty(capture,CV_CAP_PROP_FRAME_COUNT); 9

  while (true)
  {
    IplImage* ramka = cvQueryFrame(capture);

    if (ramka != 0){
      cvShowImage("stare okno", ramka);
      cvSaveImage("out_without_filter.bmp",ramka); 10
      cvFilter2D(ramka,ramka,maska); 11
      cvSaveImage("out_with_filter.bmp",ramka); 12
      //cvWriteFrame(writer,ramka);
    }
    else{
      //system("PAUSE");
      break;
    }

    int c = cvWaitKey(odstep_miedzy_klatkami);

    if (c == 'k')
      break;

    cvShowImage("nowe okno", ramka);
    cvWriteFrame(writer,ramka);
  }

  /*
  CvCapture* capture_new = cvCreateFileCapture("out.avi"); 13

  cvQueryFrame(capture_new); 14

  while (true)
  {
    IplImage* ramka_new = cvQueryFrame(capture_new);

    if (ramka_new != 0)
    cvShowImage("nowe okno", ramka_new);
    else{
      system("PAUSE");
      break;
  }

  int d = cvWaitKey(odstep_miedzy_klatkami);
  if (d == 'z')
    break;
  }
  */

  cvReleaseMat(&maska); 15
  cvReleaseVideoWriter(&writer);
  cvReleaseCapture(&capture);
  //cvReleaseCapture(&capture_new);
  cvDestroyAllWindows();

  return 1;
}

1

Tworzenie okna dla starego pliku video.

2

Tworzenie okna dla nowego pliku video.

3

Odczytanie pierwszej ramki.

4

Odczytnie ilosci ramek na sekunde.

5

Wczytanie odstepu miedzy klatkami.

6

Tworzymy macierz 3x3.

7

Zastosowanie zwyklego filtru wyostrzajacego.

8

Zastosowanie filtru Sobel. Ustawienie macierzy.

9

Wczytanie ilosci ramek w calym filmie.

10

Zapisanie obrazu przed zastosowaniem filtru.

11

Zastosowanie maski wyostrzajacej.

12

Zapisanie obrazu po zastosowaniu filtru.

13

Inicjalizacja przechwytu z nowego pliku.

14

Odczytanie pierwszej ramki nowego pliku, pobranie nastepnej oraz wyswietlenie.

15

Zwolnienie niepotrzebnych zasobow.

2. Algorytm równoległy

Naszym celem było stworzenie algorytmu przetwarzania równoległego video za pomocą jednostek wektorowych (SPE). Rozkładając obliczenia na poszczególne rdzenie, wykorzystaliśmy inną niż w poprzednim algorytmie bibliotekę - CImg.

W projekcie zostało użyte proste przetwarzanie w postaci tworzenia negatywu. Program działa na plikach mpeg. Jest przystosowany do optymalnego przetwarzania plików video o dowolnych wymiarach.

BIBLIOTEKA CImg:

  1. ZALETY:

    • Wbudowany zestaw filtrów graficznych

    • Możliwość rysowania prostych kształtów

    • Wyświetlanie obrazków i obsługa zdarzeń myszy

    • Prosta w użyciu - wystarczy dołączyć plik cimg.h

  2. WADY:

    • Brak przykładów integracji z bibliotekami do tworzenia interfejsu użytkownika

    • CImg domyślnie potrafi czytać i zapisywać kilka rodzajów plików, pozostałe formaty graficzne obsługiwane są po zainstalowaniu dodtakowych komponentów

2.1. Projekt

2.1.1. PRZYKŁAD NR.1

Rysunek 4.3. Obraz wejściowy

Obraz wejściowy

Rysunek 4.4. Obraz wyjściowy jako negatyw

Obraz wyjściowy jako negatyw

2.1.2. PRZYKŁAD NR.2

Rysunek 4.5. Obraz wejściowy

Obraz wejściowy

Rysunek 4.6. Obraz wyjściowy jako negatyw

Obraz wyjściowy jako negatyw

2.2. Implementacja

2.2.1. Plik context.h

struct context
{
  unsigned long long frame_addr; 1
  unsigned int num;  2
} __attribute__((aligned(128))); 3

1

Adres pierwszego piksela przydzielonego do danej jednostki SPE.

2

Liczba pikseli do przetworzenia przez jednostke SPE .

3

Wyrownanie struktury (kazda jej instancja bedzie zajmowala pamiec wyrownana do 128 bajtow, czyli do optymalnej wielkosci podczas przesylania danych).

2.2.2. Plik ppu.cpp

#include <cstdio>
#include <libspe2.h>
#include <pthread.h>

#include "CImg.h"
#include "context.h"

using namespace cimg_library;
using namespace std;

bool file_exists(const char * filename); 1
void print_video_info(const CImgList<unsigned char> &video);

spe_program_handle_t* spu;

void *spe_handler(void *arg) 2
{
  spe_context_ptr_t spe_ctx;
  context *data = (context *)arg;

  unsigned int entry=SPE_DEFAULT_ENTRY;;

  spe_ctx = spe_context_create(0, NULL);
  spe_program_load (spe_ctx, spu);

  spe_context_run(spe_ctx, &entry,0,data,NULL,NULL);
  spe_context_destroy(spe_ctx);

  pthread_exit(NULL);
}

int main(int argc, char** argv)
{
  if(argc == 1)
  {
    printf("Nie podales nazwy pliku wejsciowego!\n%s nazwa_filmu_wej.mpg nazwa_filmu_wyj.mpg\n",argv[0]);
    exit(1);
  }

  if(argc == 2)
  {
    printf("Nie podales nazwy pliku wyjsciowego!\n%s nazwa_filmu_wej.mpg nazwa_filmu_wyj.mpg\n",argv[0]);
    exit(1);
  }

  if(!file_exists(argv[1]))
  {
    printf("Nie mozna otworzyc pliku %s\n",argv[1]);
    exit(1);
  }

  CImgList<unsigned char> video; 3
  video.load_ffmpeg(argv[1]);

  print_video_info(video);

  int height = video[0].height;
  int width  = video[0].width;

  int num_spe = spe_cpu_info_get(SPE_COUNT_USABLE_SPES, -1); 4

  int spu_part_size = (width * height * 3) / num_spe; 5
  int rest_spu_part_size = (width * height * 3) % num_spe;

  pthread_t *pthreads = new pthread_t[num_spe]; 6
  context *ctxs = new context[num_spe];

  spu = spe_image_open("video-spu"); 7

  printf("Przetwarzanie przez %d jednostek SPU\n",num_spe);
  printf("Prosze czekac(moze to potrwac kilka minut)...\n");
  printf("---------------------------------------------\n"); 8

  int old_progress = 0;

  for(int j=0; j<video.size; j++)
  {
    unsigned char *in_data = video[j].ptr(); 9

    for(int i=0;i<num_spe;i++)
    {
      ctxs[i].num = spu_part_size; 10
      if(i == num_spe-1)
        ctxs[i].num += rest_spu_part_size;

      ctxs[i].frame_addr = (unsigned long long int)(in_data)+ctxs[i].num*i; 11
      pthread_create(&pthreads[i], NULL, &spe_handler, &ctxs[i]);
    }

    for (int i=0; i<num_spe; i++) 12
      pthread_join (pthreads[i], NULL);
  }

  printf("Zakonczono przetwarzanie! Trwa zapis pliku wideo: %s\n", argv[2]);

  video.save_ffmpeg(argv[2]); 13

  printf("Zapis zakonczony powodzeniem!\n");

  delete[] pthreads;
  delete[] ctxs;

  return (0);
}
14
bool file_exists(const char * filename)
{
  if (FILE * file = fopen(filename, "r"))
  {
    fclose(file);
    return true;
  }
  return false;
}

void print_video_info(const CImgList<unsigned char> &video) 15
{
  printf("Przetwarzanie video\n");
  printf("----------------------------------------------------------------\n");

  printf("INFO:\n");
  printf("szerokosc: %d\n", video[0].width);
  printf("wysokosc: %d\n", video[0].height);
  printf("ilosc klatek: %d\n\n", video.size);
}

1

Zapowiedzi funkcji pomocniczych i uchwyt do programu SPU.

2

Funkcja wykonywana przez pojedynczy watek(uruchomienie jednostki SPU, wyslanie danych i zakonczenie watku).

3

Zaladowanie video.

4

Liczba dostepnych do przetwarzania jednostek SPU.

5

Wielkosc czesci pikseli do przetworzenia dla kazdej jednostki, oraz liczba pikseli nadmiarowych.

6

Kontenery na watki oraz na konteksty wywolania watkow(dane do nich przesylane).

7

Dynamiczne zaladowanie programu dla SPU.

8

Wskaznik postepu, 40 znakow.

9

Adres poczatku danych kazdej klatki.

10

Uwzglednienie nierownomiernego podzialu, jesli wystapi ostatnia jednoskta SPU wykonuje nadmiar.

11

Wyliczenie odresu poczatkowego piksela dla kazdej jednostki SPU.

12

Oczekiwanie na zakonczenie wątków SPE.

13

Zapis przetworzonego video.

14

Funkcje pomocnicze.

1. Funkcja sprawdza czy plik istnieje.

15

2. Funkcja wyswietlajaca informacje o pliku wideo.

2.2.3. Plik spu.cpp

#include <cstdlib>
#include <cstdio>
#include <spu_mfcio.h>

#include "context.h"

#define MAX_CHUNK_SIZE 4096 1

unsigned char local_pixel_data[MAX_CHUNK_SIZE] __attribute__ ((aligned (128))); 2

context context_data; 3

void negatyw(unsigned char* pixel_data, unsigned int size) 4
{
  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*) pixel_data;

  int vectorchunk = (size / 16); 5

  for (int i = 0; i < vectorchunk; i++)
  {
    pixels[i] = maxPixel - pixels[i];
  }

  int rest = size%16; 6
  int rest_position = vectorchunk*16;

  for (int i = 0; i < rest; i++)
  {
    pixel_data[rest_position+i] = 255 - pixel_data[rest_position+i];
  }
}

int main(unsigned long long speid __attribute__((unused)), unsigned long long argp)
{
  unsigned long long frame_addr; 7
  unsigned int chunk_size;

  unsigned int tag = mfc_tag_reserve(); 8
  if (tag == MFC_TAG_INVALID)
  {
    printf ("BLAD SPU! \n Nie mozna przydzielic tagu dla SPU.\n");
    return 1;
  }

  mfc_get (&context_data, argp, sizeof (context_data), tag, 0, 0); 9

  mfc_write_tag_mask (1 << tag); 10
  mfc_read_tag_status_all ();

  int num_chunks = context_data.num/MAX_CHUNK_SIZE; 11

  int last_chunk_size = context_data.num%MAX_CHUNK_SIZE;

  if(last_chunk_size != 0)
    num_chunks++;

  for (int i = 0; i < num_chunks; i++) 12
  {
    if(last_chunk_size != 0 && i == (num_chunks-1)) 13
      chunk_size = last_chunk_size;
    else
      chunk_size = MAX_CHUNK_SIZE;

    frame_addr = context_data.frame_addr + (i* MAX_CHUNK_SIZE * sizeof(unsigned char));

    mfc_get (local_pixel_data, frame_addr, chunk_size * sizeof(unsigned char), tag, 0, 0); 14

    mfc_write_tag_mask (1 << tag);
    mfc_read_tag_status_all ();

    negatyw (local_pixel_data, chunk_size); 15

    mfc_put (local_pixel_data, frame_addr, chunk_size * sizeof(unsigned char), tag, 0, 0);

    mfc_write_tag_mask (1 << tag);
    mfc_read_tag_status_all ();
  }

  return 0;
}

1

Maksymalny rozmiar przetwarzanego kawałka obrazu.

2

Lokalny bufor przechowujacy otrzymywane i wysylane przez SPE dane (piksele).

3

Struktura przechowujaca informacje potrzebne do przetworzenia, otrzymane przez jednoske SPU.

4

Funkcja przetwarzajaca size pikseli wskazywanych przez pixel_data. Aby przyspieszyc dzialanie, zastosowane mozliwe w tym wypadku przetwarzanie wektorowe grupujace przetwarzane piksele.

5

Ilosc iteracji podczas wektorowego przetwarzania.

6

Jesli zostana jakies nieprzetworzone piksele, przetwarzamy je juz w normalnej petli: rest okresla liczbe pikseli pozostalych do przetworzenia, rest_position pozycje pierwszego piksela do przetworzenia.

7

frame_addr: Zmienna pomocnicza wykorzystywana w petli glownej, zawierajaca wyliczony adres pikseli do odczytania z lokalnej pamieci i wyslania spowrotem do PPU.

chunk_size: Zmienna pomocnicza wykorzystywana w petli glownej, zawierajaca wielkosc aktualnie przetwarzanego kawalka (wprowadzona by poprawnie obsluzyc nierownomierny podzial danych na kawałki).

8

Przydzielenie MFC tag'u dla naszego SPE.

9

Odczytanie z DMA informacji dotyczacych przetwarzanych pikseli przekazanych z SPU.

10

Oczekiwanie na zakonczenie przesylania danych.

11

Obliczenie wielkosci pojedynczego kawałka danych do przetworzenia oraz ostatniego kawalka do przetworzenia (jesli podzial jest nierownomierny).

12

Petla glowna przetwarzania klatki przez SPE. Dane dzielone są na kawałki (chunks), przetwarzane i nastepnie kopiowane z pamieci lokalnej SPE do pamieci systemowej PPU. Podzielenie na kawalki i buforowanie zapewnia bardziej plynne przetwarzanie danych.

13

Obliczenie adresu aktualnie przetwarzanej porcji danych.

14

Przeslanie przetworzonego kawałka dnych z pamieci lokalnej SPE do pamieci systemowej PPU.

15

1. Przetwarzanie otrzymanych danych.

2. Przeslanie przetworzonego kawałka dnych z pamieci lokalnej SPE do pamieci systemowej PPU.

3. Oczekiwanie na zakonczenie przesylania danych.