Zagadnieniem do którego dotarłem w ramach moich eksploracji EmguCV jest rysowanie.
Podstawowym narzędziem rysującym jest metoda Draw. Metoda ta ma 11 przeciążeń, które osobiście podzieliłbym na 3 grupy: Rysowanie kształtów, Wyświetlanie tekstu, Rysowanie sekwencji.
Rysowanie kształtów:
Metoda Draw posiada 7 dedykowanych metod przeznaczonych do rysowania podstawowych kształtów geometrycznych. Metody te jako pierwszy parametr przyjmują strukturę opisującą kształt oraz 2 dodatkowe parametry: kolor lini, oraz jej grubość. Przy czym grubość 0 lub mniej oznacza wypełnienie kształtu kolorem.
var rect = new System.Drawing.Rectangle(x, y, width, height);
image.Draw(rect, color, thickness);
I w zasadzie jedyne co mogę stwierdzić to fakt, że działa.
Przypadkiem w którym nie jest już tak różowo jest koło/okrąg ponieważ to coś
co powstaje w wyniku wywołania tego kodu:
CircleF c = new CircleF(new PointF(centerX, centerY), radius);
image.Draw(c, color, thickness);
ciężko nazwać okręgiem. Przydałoby się tutaj trochę antialiasingu, niestety klasa Image nie posiada żadnej metody ani właściwości gwarantującej taką funkcjonalność, a więc trzeba kombinować.
Pierwszym obejściem jakie znalazłem było wykorzystanie właściwości Bitmap która jest typu System.Drawing.Bitmap. Właściwość ta dla kolorów Brg, Brga oraz Gray zwraca wewnętrzną Bitmapę synchronizowaną dwustronnie z zawartością obrazu, dla pozostałych sposobów opisu koloru zwracana jest nowa/niesynchronizowana Bitmapa. Podejrzewam że ta właśnie synchronizacja jest przyczyną przegranej Emgu z metodą BitmapData w teście wydajności.
Mając obiekt typu Bitmap możemy wykorzystać całe bogactwo biblioteki GDI+ w tym klasę Graphics. Narysowanie okręgu z antialiasingiem można wykonać takim kodem:
using (var graphics = Graphics.FromImage(image.Bitmap))
{
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
using (var pen = new Pen(color, thickness))
{
graphics.DrawEllipse(pen, x, y, diameter, diameter);
}
}
a efekt jego działania prezentuje się następująco
Drugim rozwiązaniem jakie udało mi się znaleźć jest użycie klasy cvInvoke. O ile klasa Image jest dobrze przemyślanym proxy do wielu funkcjonalności z OpenCV tak klasa cvInvoke ma na celu zapewnić dostęp do jak największej grupy metod, niekoniecznie trzymając się najlepszych wzorców. Większość (jak nie wszystkie) z metod tej klasy korzysta bezpośrednio z PInvoke, jest pośród nich także metoda cvImage.
var center = new Point(centerX, centerY);
int radius = dlg.Radius;
var color = new Bgr(); // Jakiś kolor
var lineType = Emgu.CV.CvEnum.LINE_TYPE.CV_AA;
CvInvoke.cvCircle(image.Ptr, center, radius, color.MCvScalar, thickness, lineType, 0);
Metoda jako pierwszy parametr pobiera wskaźnik, można go znaleźć w klasie Image<,> pod właściwością Ptr, drugim parametrem są współrzędne środka okręgu, a kolejnym jego promień.
Czwartym parametrem jest kolor, nie jest to jednak ani liczba opisująca kolor, ani obiekt z grupy kolorów tylko coś nazwane MCvScalar, jak widać w powyższym listingu wartość tą można uzyskać z koloru przy pomocy właściwości MCvScalar.
Piątym parametrem jest grubość linii, interpretowana tak samo jak w metodzie Draw, a szóstym jej typ. Są 3 typy linii 4-połączeniowa, 8-połączeniowa (używana przez metodę Draw) oraz AntiAliasing. Wyniki metody cvCircle z różną wartością tego parametru (w powiększeniu) przedstawia grafika poniżej.
Organoleptycznie stwierdzam, w tym miejscu błąd w OpenCV ponieważ przy wybraniu opcji 4-połączeniowej wyraźnie widać połączenia po ukosie, których nie powinno być.
Ostatnim parametrem jest przesunięcie służące do skalowania wynikowego okręgu jeżeli 1 piksel ma być równy 1 jednostce we współrzędnych środka i długości promienia należy ustawić ten parametr na 0.
Rysowanie napisów
Jak każda biblioteka graficzna EmguCV umożliwia umieszczanie napisów na grafice. Sama klasa Image posiada przeciążenie metody Draw która jako parametry pobiera:
- napis który ma zostać narysowany
- opis czcionki – struktura zawierająca dane o czcionce
- pozycję tekstu
- kolor
Rysowanie sekwencji
Sekwencja (Typ Seq<T>) jest kolekcją pozwalającym przechowywać elementy w sposób uporządkowany. Elementy można dodawać na końcu (Metoda Push) albo na początku (PushFront) sekwencji. Sekwencja wymaga wskazania Magazynu (klasa MemStorage).
using (var memStor = new MemStorage())
{
var seq = new Seq(memStor);
var rand = new Random();
for (int i = 0; i < 100; i++)
{
var point = new Point(rand.Next(100), rand.Next(100));
seq.Push(point);
}
}
Metoda Draw posiada 3 metody do rysowania sekwencji punktów. W najprostrzym przypadku przyjmuje ona 3 parametry: wspomnianą sekwencję oraz grubość i kolor linii. Dwa ostatnie parametry są interpretowane analogicznie jak we wczesniejszych przeciążeniach. Co ciekawe przekazanie obiektu Seq<Point> wypełnionymi punktami sprawi, że grafika nie ulegnie zmianie (WTF?).
Po wczytaniu się w dokumentację znalazłem przyczynę. Sekwencja sama w sobie stanowi tylko abstrakcję dla innych implementacji. Jednak jako taka nie jest abstrakcyjna można ją tworzyć, można do niej dodawać elementy, jednak nie da się jej narysować. Kolejną ciekawostką jest fakt iż jedyną klasą dziedziczącą po sekwencji jest Contour<T>, a próba dziedziczenia i nadpisania jedynej wirtualnej metody InContour nic nie zmienia (podczas rysowania ta metoda nie jest nawet wywoływana). Na szczęście użycie klasy Contour<T> daje efekt w postaci narysowania wielokąta. Którego wierzchołkami są punkty sekwencji.
DrawPolyline
Jak sama nazwa wskazuje służy do rysowania wielu linii, czyli łamanej. Posiada 2 przeciążenia. Pierwsze z nich pobiera jako parametr tablicę punktów, flagę określającą czy domknąć łamaną (czyli zrobić z niej wielokąt), oraz kolor i grubość linii interpretowane tak samo jak przez metodę Draw. Z punktu widzenia programisty można tą metodę traktować jak uproszczoną wersję rysowania konturu.
Drugie przeciążenie różni się pierwszym parametrem jako który przyjmuje tablicę tablic punktów i służy do rysowania wielu łamanych.
FillConvexPolygon
Metoda pozwala wypełnić kolorem wielokąt wypukły nie należy jednak używać tej metody do rysowania wielokątów wklęsłych albo figur z przecinającymi się krawędziami, metoda ta prawdopodobnie wykorzystuje algorytm skanowania linii (o ile dobrze pamiętam nazwę) wydajny ale ograniczony funkcjonalnie. Moim zdaniem metoda ta ma wąskie zastosowanie.
Przykładowe listingi prezentujące wykorzystanie metod jak zawsze znajdują się w projekcie EmguExperiments