Amatorskie spojrzenie na EmguCV – operacja na pikselach oraz wydajność

Pomimo bardzo bogatego zestawu metod klasy Image nie robi ona wszystkiego. Więc istnieje konieczność operacji bezpośrednio na punktach składających się na obraz. EmguCV oferuje dwie możliwości:

  • Czytelną w której zostaje użyty indekser zwracający obiekt TColor(w przykładzie poniżej Bgr)

    var pixel = image[row, column];
    pixel.B = value;
    image[row, column] = pixel;
    
  • Mniej czytelną ale wydajnieszą, odwołanie następuje do obiektu Data indekser na nim pozwala pominąć operacje na obiekcie i operować bezpośrednio na tablicy znajdującej się pod spodem.

    byte bluePartOfPixel = image.Data[row, column, 0];
    image.Data[row, column, 0] = (byte)78;
    
  • Najwyższa pora sprawdzić wydajność Emgu CV. Postanowiłem zaimplementować metodę generującą negatyw zdjęcia na kilka sposobów:

    • GetPixel/SetPixel – metoda z góry skazana na przegraną, jej jedynymi zaletami są: nie zła czytelność kodu, oraz fakt że dostępna w .Net z pudełka
    • BitmapData – wydajna metoda, główny konkurent Emgu CV pod tym względem, oprócz wydajności posiada same wady: wymaga używania sekcji unsafe, jest mało czytelna i błędogenna. Ze względu na ostatnią cechę zamiast się bawić w ręczną implementację wykorzystałem gotowca ze Stacka
    • ColorMatrix – stare dobre GDI+ umożliwia dokonywania przekształceń matrycowych zarówno na orientacji pikseli jak i ich kolorów (potrzebna jest do tego klasa Graphics) implementację znalazłem pod tym samym linkiem, co w przypadku BitmapData
    • Pixel Access pierwsza wspomniana w tym poście metoda dostępów do pikseli w EmguCV
    • Pixel Access Fast – druga metoda
    • Metoda Not – metoda w klasie Image dedykowana generowaniu negatywu obrazu

    Testy wykonywałem na swoim laptopie (8 GB RAM, i5, dwie karty grafiki NVidia NVS 5400M oraz jakiś Intel). Test podzieliłem na 2 kroki Otwarcie pliku – polegające na wywołaniu konstruktora klasy z odpowiednim parametrem, oraz Wygenerowanie negatywu – właściwa robota. Odrzucałem wynik pierwszego uruchomienia Testu (podczas niego mogą wykonywać się operacje które nie występują przy kolejnych uruchomieniach). Test wykonałem tylko na trzech plikach, oraz uruchamiając program na różnych kartach grafiki (mam możliwość wskazania która karta powinna być wykorzystana).
    Metoda badań nie jest może idealna jednak mimo to daje całkiem ciekawe wyniki:

    Obraz 3702×2304 jpeg 916KB

    NVidia NVS 5400M

    GDI+ Emgu CV
    Metoda GetPixel/SetPixel BitmapData ColorMatrix Pixel Access Pixel Access Fast Metoda Not
    Otwarcie pliku 00.037 00.038 00.039 00.107 00.110 00.112
    Wygenerowanie negatywu 10.404 00.054 00.157 02.892 02.088 00.114

    Intel(R) HD Graphics 4000

    GDI+ Emgu CV
    Metoda GetPixel/SetPixel BitmapData ColorMatrix Pixel Access Pixel Access Fast Metoda Not
    Otwarcie pliku 00.037 00.036 00.038 00.110 00.113 00.110
    Wygenerowanie negatywu 10.470 00.053 00.157 03.071 02.296 00.112

    Obraz 4320×3240 jpeg 3,21 MB

    NVidia NVS 5400M

    GDI+ Emgu CV
    Metoda GetPixel/SetPixel BitmapData ColorMatrix Pixel Access Pixel Access Fast Metoda Not
    Otwarcie pliku 00.037 00.038 00.039 00.107 00.110 00.112
    Wygenerowanie negatywu 20.764 00.118 00.340 06.079 04.302 00.252

    Intel(R) HD Graphics 4000

    GDI+ Emgu CV
    Metoda GetPixel/SetPixel BitmapData ColorMatrix Pixel Access Pixel Access Fast Metoda Not
    Otwarcie pliku 00.088 00.087 00.086 00.248 00.244 00.247
    Wygenerowanie negatywu 20.644 00.120 00.344 06.041 04.066 00.252

    Obraz 5184×3456 jpeg 4,87 MB

    NVidia NVS 5400M

    GDI+ Emgu CV
    Metoda GetPixel/SetPixel BitmapData ColorMatrix Pixel Access Pixel Access Fast Metoda Not
    Otwarcie pliku 00:00.112 00:00.117 00:00.115 00:00.345 00:00.353 00:00.355
    Wygenerowanie negatywu 00:00.112 00:00.117 00:00.115 00:00.345 00:00.353 00:00.355

    Intel(R) HD Graphics 4000

    GDI+ Emgu CV
    Metoda GetPixel/SetPixel BitmapData ColorMatrix Pixel Access Pixel Access Fast Metoda Not
    Otwarcie pliku 00:00.114 00:00.119 00:00.118 00:00.348 00:00.351 00:00.342
    Wygenerowanie negatywu 00:00.114 00:00.119 00:00.118 00:00.348 00:00.351 00:00.342

    Zgodnie z moimi oczekiwaniami BitmapData pokonał inne metody GDI+. Wywołanie dedykowanej metody Not detronizuje zabawę bezpośrednio na Pikselach, jak dla mnie jest to jednoznaczne ze stwierdzeniem "Jak coś jest już napisane nie pisz tego sam".
    Bardzo interesujący natomiast jest fakt iż metoda BitmapData jest dwukrotnie szybsza od dedykowanej metody z Emgu CV. Jestem jednak daleki od stwierdzenia iż mój test udowadnia słabość tej biblioteki. Wskazany wynik może mieć wiele przyczyn, metoda Not może być na tyle prosta że Emgu CV nie ma co optymalizować (na co wskazywałby brak istotnych różnic przy uruchamianiu programu na różnych kartach grafiki), jednak ambitniejszych przekształceń po prostu nie chce mi się implementować "z palca" więc nie dokonam takiego porównania.
    Warto też zwrócić uwagę na fakt iż przekształcenie matrycowe także nie wypadło źle w zestawieniu, a jego czytelność jest przyzwoita (co nie zmienia faktu iż kod pisany z jego wykorzystaniem zawsze powinien być komentowany).
    Ostatnim elementem układanki jest czas pobrania danych z dysku. W przypadku Emgu CV odczytanie danych stanowi dużą część pracochłonności całej operacji. W moim teście popełniłem jeden błąd i większe zdjęcia wczytywałem z dysku SSD, a małe z talerzowego więc nie może on być podstawą wnioskowania o skalowalności tego rozwiązania.
    Całość kodu testu znajduje się pod adresem: https://github.com/szogun1987/EmguExperriments/tree/master/EmguExperiments/Performance