dareks_

  • Dokumenty2 821
  • Odsłony753 730
  • Obserwuję431
  • Rozmiar dokumentów32.8 GB
  • Ilość pobrań361 988

Programista 2012 01

Dodano: 6 lata temu

Informacje o dokumencie

Dodano: 6 lata temu
Rozmiar :31.4 MB
Rozszerzenie:pdf

Programista 2012 01.pdf

dareks_ CZASOPISMA
Użytkownik dareks_ wgrał ten materiał 6 lata temu.

Komentarze i opinie (0)

Transkrypt ( 25 z dostępnych 64 stron)

C++11 w praktyce: Sygnały i sloty M a g a z y n p r o g r a m i s t ó w i l i d e r ó w z e s p o ł ó w I T 1/2012 (1) marzec www•programistamag•pl NR Domain Driven Design Wyrażenia regularne w C++ Arduino Pierwszy kontakt Praktyczne wy- korzystanie RUP W artykule przedstawiono podstawowe Building Blocks DDD wraz z przykładami ich implementacji w Javie z wykorzystaniem Spring i JPA Artykuł opisuje prakty­ czne wyko­rzystaniw metod i narzędzi Inżynierii Opro­ gramowania na stanowisku pracy Analityka Biznesowego Arduino to platforma oparta o mikrokontroler z rodziny AVR. W artykule zaprezentowano podstawowe wiadomości o Arduino i przykłady tworze­ nia programów dla tego typu urządzenia W artykule omawiamy biblioteki boost::regex i boost::xpressive, dostarczające wyrażeń regu­ larnych dla C++ Pierwszy numer bezpłatny

REDAKCJA/EDYTORIAL Szanowni Czytelnicy. Mam przyjemność oddać w Wasze ręce premierowe wydanie magazynu „Programista”. Progra- mista to nowy projekt. Pismo skierowane jest do zawodo- wych programistów i członków zespołów IT. Przygotowania do tego dnia zajęły wiele miesięcy, ale jestem przekonany, że ostate­czny efekt zyskaWasze uznanie. Skąd pomysł na magazyn? Jesteśmy zespołem ludzi z wie- loletnim doświadczeniem w branży wydawniczej związanej z IT i uznaliśmy, że na polskim rynku brakuje profesjonalnych wydawnictw skierowanych do tej grupy docelowej. Rozumiemy, jak ważny jest czas w pracy, dlatego naszym celem jest dostarczanie profesjonalnych i ciekawych materia- łów wprost do ich rąk. Materiałów, dzięki którym będą mogli poszerzyć swoją wiedzę i zdobyć nowe umiejętności. Stawia- my na nowości, ale nie zapominamy o starszych rozwiąza- niach. Jakość jest słowem, które jest nam bardzo bliskie. Dlate- go zwracamy szczególną uwagę na to, w jaki sposób wiedza dociera do czytelnika. Nasz magazyn jest prosty i elegancki w formie. W premierowym wydaniu znajdziecie wiele interesują- cych artykułów. Przeczytacie teksty dotyczące C++11, biblio- tek boost::regex i boost::xpressive, analizy obrazu, Arduino czy też wprowadzenie do AMFPHP. Podjęliśmy współpracę z najlepszymi autorami, ale także z firmami z wieloletnim doświa­dczeniem i odpowiednią wiedzą dotyczącą pracy ze- społów IT. W magazynie znajdziecie pierwszy artykuł z serii Klub Li- dera IT. Będziemy zamieszczać w nim teksty koncentrujące się na tym co trapi liderów zespołów programistycznych. Bę- dziemy zajmować się budowaniem i zarządzaniem zespołem programistycznym, kłopotami z architekturą i architektami, motywowaniem programistów, promowaniem własnych po- mysłów w organizacji, zarządzaniem dokumentacją, konfli­ ktami z tak zwanym Biznesem i efektywnością zespołu pro- gramistycznego. Aby zadowolić naszych Czytelników, z uwagą będziemy wsłuchiwać się w ich uwagi i potrzeby. Dlatego wszelkie ko- mentarze i sugestie są przez nas mile widziane. Nie obawia- my się również krytyki. Jesteśmy zdania że każda opinia jest wartościowa i dzięki każdej uwadze możemy ulepszać nasz magazyn. Od następnego numeru (już w maju!) "Programista" bę- dzie ukazywał się również w wersji drukowanej i będzie liczył 80 stron. Zachęcamy do prenumeraty pisma.Więcej infomacji znajdą Państwo na stronie www.programistamag.pl. Zapraszam do lektury. Startujemy! Łukasz Łopuszański Redaktor naczelny lukaszlopuszanski@programistamag.pl Magazyn Programista wydawany jest przez firmę Anna Adamczyk Wydawca: Anna Adamczyk annaadamczyk@programistamag.pl Redaktor naczelny: Łukasz Łopuszański lukaszlopuszanski@programistamag.pl Z-ca Redaktora naczelnego: Tomasz Łopuszański tomaszlopuszanski@programistamag.pl Korekta: Tomasz Łopuszański Kierownik produkcji: Krzysztof Kopciowski bok@keylight.com.pl DTP: Krzysztof Kopciowski Dział reklamy: reklama@programistamag.pl tel. +48 663 220 102 tel. +48 604 312 716 Prenumerata: prenumerata@programistamag.pl Współpraca: Rafał Kocisz, Michał Bartyzel, Mariusz Sieraczkiewicz, Sławomir Sobótka, Artur Machura Adres korespondencyjny: Magazyn „Programista”, ul.Meissnera 7 lok 5, 03-982 Warszawa

SPIS TREŚCI O ile nie zaznaczono inaczej, wszelkie prawa do wszystkich materiałów zamieszczanych na łamach magazynu Programista są zastrzeżone. Kopiowanie i rozpowszechnianie ich bez zezwolenia jest wzbronione. Naruszenie praw autorskich może skutkować odpowiedzialnością prawną, określoną w szczególności w przepisach ustawy o prawie autorskim i prawach pokrewnych, ustawy o zwalczaniu nieuczciwej konkurencji i przepisach kodeksu cywilnego oraz przepisach prawa prasowego. Redakcja magazynu Programista nie ponosi odpowiedzialności za szkody bezpośrednie i pośrednie, jak również za inne straty i wydatki poniesione w związku z wykorzystaniem informacji prezentowanych na łamach magazy­ nu Programista.Wszelkie nazwy i znaki towarowe lub firmowe występujące na łamach magazynu są zastrzeżone przez odpowiednie firmy. AKTUALNOŚCI............................................................................................................................................4 Rafał Kocisz BIBLIOTEKI I NARZEDZIA Wyrażenia regularne w C++ biblioteka boost::regex i boost::xpressive............................. Robert Nowak 6 PROGRAMOWANIE GRAFIKI Analiza obrazu: rozpoznawanie obiektów................................................................................... PawełWolniewicz 10 JĘZYKI PROGRAMOWANIA C++11 w praktyce: sygnały i sloty.................................................................................................. Rafał Kocisz 20 PROGRAMOWANIE SYSTEMÓW OSADZONYCH Arduino–Pierwszykontakt............................................................................................................. Marek Sawerwain 28 TECHNOLOGIE FLASH/FLEX WprowadzeniedoAMFPHP............................................................................................................. Paweł Murawski 34 INŻYNIERIA OPROGRAMOWANIA Domain Driven Design krok po kroku Część I: Podstawowe Building Blocks DDD.......... Sławomir Sobótka 38 Praktyczne wykorzystanie RUP na stanowisku pracy Analityka Biznesowego................. Artur Machura 48 KLUB LIDERA IT Ewolucyjna architektura Jak zorganizować proces rozwoju architektury?........................ Michał Bartyzel, Mariusz Sieraczkiewicz 54 E-LEARNING DevCastZone – nowy wymiar e-edukacji dla programistów...................................................60 IT BOYZ .......................................................................................................................................................62 Zuch

4 / 1 . 2012 . (1)  / AKTUALNOŚCI Objective-C czarnym koniem rankinguTiobe w roku 2011 Jeśli ktoś ma jeszcze wątpliwości co do tego, że tech- nologie wywierają przemożny wpływ na popularność języków programowania, to może przestać głowić się nad tym problemem. Jak pokazują wyniki rankingu po- pularności języków programowania TIOBE, ewidentnym zwycięzcą w kategorii języka, który odnotował najwię­ kszy wzrost zainteresowania wśród programistów w roku 2011, jest Objective-C. Nietrudno się domyśleć, iż wzrost ten jest bezpośrednio powiązany z falą sukcesów, które święcą ostatnio platformy iOS oraz OS X. A co z resztą tabeli? Na pierwszym miejscu króluje nie- przerwanie Java, jednakże jej notowania z roku na rok sukcesywnie maleją. Dalej mamy C, oraz C#, które wy- parło z trzeciego miejsca C++. JavaScript powrócił do pierwszej dziesiątki, wypierając Ruby. Największymi przegranymi tego roku są niewątpliwie Python (zeszło- roczny lider w tabeli) oraz PHP. TIOBE to dość kontrowersyjny ranking, wiele osób nie traktuje go poważnie, krytykując chociażby stosowa- ny przezeń mechanizm szacowania poziomu popularno- ści. Jednakże, patrząc z perspektywy kilku ostatnich lat, TIOBE wydaje się być dość skutecznym aparatem wspo- magającym monitorowanie głównych trendów czy też ogólnie pojętej mody w dziedzinie języków programowa- nia. Podsumowanie tegorocznych wyników rankingu wy- daje się potwierdzać powyższą tezę. HTTP://WWW.TIOBE.COM Zapowiedź Apple OS X Mountain Lion: marzenie użytkowników, utrudnienia programistów Wdość nietypowy dla siebie sposób – w formie noty prasowej - firma Apple przedstawiła zapowiedź no- wej wersji systemu z rodziny OS X: 10.8, noszącego ko- dową nazwę Mountain Lion. Warto na samym początku odnotować pewien ciekawy fakt; otóż z nazwy systemu usunięto słowo Mac. Od wersji 10.8 mamy do czynienia po prostu z OS X. W pierwszej kolejności dostęp do próbnej wersji nowego systemu otrzymali przedstawiciele najpopularniejszych blogów technicznych; mniej więcej tydzień później wersja Developer Preview została przekazana uczestnikom Mac Developer Program. Warto odnotować, że taka a nie inna kolejność wzbudziła pewne kontrowersje, gdyż w przy- padku wcześniejszych wersji OS X’a to programiści otrzy- mywali w pierwszym rzędzie możliwość zapoznania się ze zmianami w nowej odsłonie systemu; dopiero zaś po przeanalizowaniu ich uwag i komentarzy produkt trafiał w ręce pierwszych recenzentów. Czym zamierza zadziwić nas Apple w nowej odsłonie OS X? Analizując pierwsze recenzje oraz zestawienia wła- ściwości nowego systemu, można odnieść wrażenie, iż Mountain Lion to krok w kierunku… iOS. Praktycznie wszystkie nowe właściwości tego systemu, czyli Notifica- tion Center, AirPlay Mirroring, czy Game Center AirPlay mirroring, and Game Center, stanowią wierne odwzoro- wanie usług dostępnych u jego mobilnego kuzyna. Jak by tego było mało, Apple zdecydowało się zmienić na- zwy istnie­jących aplikacji dostępnych w OS X: iChat, iCal, oraz Address Book, tak aby były one zgodne z nazwami ich odpowiedników w iOS, czyli Messages, Calendar oraz Contacts. Nowy OS X oferuje również pełniejszą integra- cję z usługą iCloud, w czego konsekwencji usługi Remin- ders oraz Notes stają się pełnoprawnymi aplikacjami zin- tegrowanymi z chmurą Apple. Całkowitą nowością oferowaną w ramach Mountain Lion’a jest Gatekeeper: mechanizm, który pozwala blo- kować uruchamianie podejrzanych aplikacji. Gatekeeper jest domyślnie skonfigurowany w taki sposób, że "prze- puszcza" jedynie aplikacje pobrane za pośrednictwem AppStore bądź podpisane przez producenta. Apple zapowiada, że z punktu widzenia programistów tworzących aplikacje pod OS X’a proces ich podpisywa- nia ma być bardzo prosty; jednakże, póki co, nie zostało jasno określone to, czy firma z Coupertino będzie rościć sobie prawo do ich moderowania. Jako główny czynnik motywacyjny implikujący wprowadzenia mechanizmu Gatekeeper’a Apple wskazuje chęć walki ze szkodliwym oprogramowaniem. Jednakże, dla programistów aplikacji OS X oznacza to, że mniej więcej do lata 2012 mają czas na podpisanie swoich produktów, gdyż w tym właśnie cza- sie Apple planuje oficjalnie wydać Mountain Lion’a. Podsumowując: patrząc z perspektywy użytkownika, no­ wy system ze stajni Apple wygląda absolutnie świetnie; patrząc z perspektywy twórców oprogramowania – spra- wa przedstawia się nieco gorzej. Wygląda na to, że ci, którzy nie chcą zadzierać z Górskim Lwem, powinni zaka- sać rękawy i brać się do pracy. HTTP://WWW.EXTREMETECH.COM Rust: nowy język programowania ze stajni Mozilla, który uratuje FireFox’a? Po ponad pięciu latach prowadzenia prac rozwojowych Mozilla Labs Rust udostępniła wersję Alpha (0.1) kom- pilatora dla języka programowania Rust. Autorzy tego języka postawili na wsparcie dla przetwarzanie równo- ległego oraz bezpieczne zarządzanie zasobami pamięci. Jeśli wszystko pójdzie zgodnie z planem, to Rust w nie- dalekiej przyszłości ma zastąpić C++ jako główny język kompilowany, wykorzystywany przy produkcji oprogra- mowania ze stajni Mozilla. W praktyce oznacza to, że cały FireFox, lub jego części mogą być przepisane w Rust. Rust jest kompilowanym, statycznie typowanym, obiekto- wym językiem programowania. Zgodnie z intencją auto- rów, nie wprowadza on żadnych rewolucyjnych konstrukcji syntaktycznych, bazuje raczej na znanych, wypróbo-

5/ www.programistamag.pl / Przygotował i zredagował Rafał Kocisz wanych w innych językach rozwiązaniach. Rust posiada opcjonalny mechanizm odśmiecania pamięci, który moż- na wykorzystywać w sposób wybiórczy (per zadanie). Twórcy Rust’a wskazują Newsqueak, Alef oraz Limbo jako języki, które najmocniej wpłynęły na jego rozwój. Kompilator Rust póki co dostępny jest dla syste- mów Windows, Linux oraz OS X. Analizując decyzję zastąpienia C++ przez Rust trudno oprzeć się wrażeniu, że Mozilla wymyśliła dość skompli- kowany sposób na wymyślenie od nowa koła, wyważając przy tym niejedne otwarte drzwi… Z drugiej strony – jak wiadomo, do odważnych świat należy, a niekonwencjo- nalne pomysły są nierzadko kluczem do sukcesu. Bardziej niepokoi jednak fakt, iż to, co autorzy Firefoxa pragną uzyskać, korzystając z nowego języka, czyli lepszą wy- dajność i stabilność aplikacji, przynajmniej w teorii po- winno dać się uzyskać stosując język C++ we właściwy sposób. Pojawia się również pytanie, czy odzyskanie sta- bilności i wydajności pozwoli dogonić Ognistemu Liskowi konkurencję, która skupia się raczej na dodawaniu no- wych, ciekawych właściwości... HTTP://WWW.EXTREMETECH.COM Microsoft udostępnia specyfikację rozszerzenia C++ AMP dla producentów kompilatorów Zgodnie z zeszłorocznymi zapowiedziami Firma Mi- crosoft postanowiła otworzyć specyfikację C++ AMP (C++ Accelerated Massive Parallelism) tak aby twórcy konkurencyjnych kompilatorów (Embarcadero, Intel czy Free Software Foundation) mogli zaimplementować to rozszerzenie w swoich produktach. Wstępna implemen- tacja tej specyfikacji przedstawiona została społeczności programistów we wrześniu 2011, jako część demonstra- cyjnej wersji pakietu Microsoft Visual Studio 2011. Głów- nym zadaniem rozszerzenia jest przyśpieszenie kodu pisanego w języku C++ za pośrednictwem GPU, przy czym inżynierowie pracujący nad stworzeniem tej spe- cyfikacji dołożyli szeregu starań, aby zachować jak naj- wyższą kompatybilność z kanoniczną wersją języka C++. Nie jest tajemnicą, że Microsoft współpracuje dość ściśle z komitetem standaryzacyjnym języka C++ i liczy na to, że członkowie tego komitetu wykorzystają pomysły za- stosowane w AMP, pracując nad kolejnymi rozszerzeniami specyfikacją języka. Fakt otwarcia specyfikacji AMP po- twierdza konsekwencję, z jaką Gigant z Redmond dąży do tego celu. HTTP://DRDOBBS.COM Osiągnięcia wVisual Studio Trudno znaleźć miłośnika gier komputerowych, który nie wiedziałby, co to są osiągnięcia (ang. achieve- ments). Ten sprytny mechanizm stosowany w grach vi- deo już od dawna sprawia, iż gracze potrafią raz po raz powracać do tych samych fragmentów rozgrywki tylko po to, aby znaleźć ukryte przejścia, zdobyć trudno dostępne monety czy przejść cały etap, korzystając tylko z wybra- nego rodzaju broni. Rzecz staje się jeszcze ciekawsza, jeśli gracz otrzymuje możliwość podzielenia się swoimi wynikami ze znajomymi w sieci. Czemu podobnej frajdy nie mogą sobie zafundować pro- gramiści? Otóż mogą! Użytkownicy pakietu Visual Studio 2010 (Professional, Premium oraz Ultimate) od niedawna mogą cieszyć się darmową wtyczką Visual Studio Achieve­ ments, która pozwoli im cieszyć się podobną funkcjonalno- ścią. Póki co do odblokowania są 32 osiągnięcia, a według zapewnień autorów liczba ta ma być sukcesywnie zwięk- szana. Wtyczka daje też możliwość publikowania osiągnięć na Facebook’a czy na własnej stronie WWW. Kryteria do- boru wymagań dla poszczególnych osiągnięć zostały po- traktowane ze sporym przymrużeniem oka – wydaje się, że autorzy wtyczki za cel postawili sobie wprowadzenie nieco luzu i zabawy w trudną i nierzadko stresującą pra- cę programisty. W tym przypadku można zaryzykować stwierdzenie, że cel ten udało się im osiągnąć. HTTP://CHANNEL9.MSDN.COM/ACHIEVEMENTS/ VISUALSTUDIO Polscy deweloperzy w czołówce twórców aplikacji na Windows Phone Polska jest trzecim na świecie krajem pod względem ilości aplikacji wydanych na platformę Windows Phone. Do końca zeszłego roku w Marketplace, wirtualnym skle- pie z programami na telefon z systemem operacyjnym Microsoftu, opublikowano 2322 aplikacje autorstwa Po- laków. Od naszych rodzimych programistów prężniej działają jedynie przedstawiciele Stanów Zjednoczonych i Indii, czyli krajów znacznie większych. Wirtualny sklep Windows Phone przekroczył pod koniec 2011 roku liczbę 50 tysięcy aplikacji gotowych do pobra- nia. Dziennie pojawia się tam około 200-300 nowych apli- kacji, zaś liczba ta nadal rośnie. Liczby te nie są tak impo- nujące jak w przypadku iOS, jednakże widać, iż ostatnie wydarzenia związane z mobilną platformą Microsoftu (geograficzne rozszerzenie dostępności do Marketplace oraz silne techniczno/marketingowe wsparcie ze strony Nokii) pobudziły jej witalność. Na pytanie, czy Windows Phone da radę przebić się na rynku i stać się konkurencją dla iOS’a oraz Androida, jedynie przyszłość może odpo- wiedzieć, jednakże na wszelki wypadek warto trzymać rękę na pulsie. HTTP://WEBHOSTING.PL

6 / 1 . 2012 . (1)  / BIBLIOTEKI I NARZĘDZIA Robert Nowak Wyrażenia regularne w C++ biblioteka boost::regex i boost::xpressive Wyrażenia regularne uważa się za elementy programowania deklaratywnego, ponie- waż nie dostarcza się sposobu postępowania (algorytmu), tylko danych wejściowych i opisu wyniku. Są one przydatne przy obróbce danych tekstowych, pozwalają zna­ cznie uprościć kod programu. W artykule omawiamy dwie biblioteki dostarczające wyrażeń regularnych dla C++ mając na uwadze fakt, że udogodnienia takie zostały dodane do standardu tego języka. Szybki start Aby uruchomić przedstawione przykłady, należy mieć do- stęp do kompilatora C++ oraz edytora tekstu. Przykłady wy- korzystują biblioteki boost (www.boost.org). Aby poprawnie uruchomić te, które wykorzystują bibliotekę boost::regex, należy dodać odpowiednie zależności dla konsolidatora (linkera), dla g++ należy dodać opcję: -lboost_regex; dla Visual Studio (program link) biblioteka ta jest dodawana automatycznie. Przykłady wykorzystujące boost::xpressive wymagają jedynie poprawnej instalacji tej biblioteki. Na wydrukach pominięto dołączanie odpowiednich nagłówków oraz udostępnianie przestrzeni nazw, pełne źródła umie­ szczono jako materiały pomocnicze. J ęzyk C++ pozwala na przetwarzanie danych teksto- wych, biblioteka standardowa dostarcza klasę do re- prezentacji napisu oraz wyrażenia regularne, są one częścią standardu C++11. Tradycyjne napisy przetwarza się za pomocą języków skryptowych (AWK, sed, Python, Perl), które stanowią rozszerzenie systemu operacyjnego i są używane do kontroli pracy aplikacji, co często wy- maga obróbki tekstowych plików konfiguracyjnych i tek- stowych plików z logami. Jeżeli wydajność modułu ope- rującego na tekście i utworzonego w tych jezykach jest niewystarczająca, warto rozważać użycie C++ do jego implementacji, wtedy kod jest translowany do kodu ma- szynowego, natomiast języki skryptowe są interpretowa- ne – więc zazwyczaj dużo mniej wydajne. Napisy w C++ są obiektami standardowej klasy std::string. Klasa ta jest konkretyzacją szablonu basic_ string dla typu char (znaki 8 bitowe). Klasa std::wstring jest generowana na podstawie tego samego szablonu – znaki są przechowywane przez obiekty typu wchar_t (znaki 16 bitowe). Oprócz możliwości przechowywania napisów (o zmiennej długości) szablon basic_string do- starcza podstawowych operacji: porównywania, łączenia napisów, wyszukiwania znaków. Język C++ pozwala na zapis stałych napisowych bez- pośrednio w kodzie programu. Ciąg znaków w cudzysło- wach jest zamieniany na tablicę znaków, którą można inicjować obiekty typu string (w opisie będziemy pomijali przestrzeń nazw). Można wskazać sposób kodowania oraz reprezentację znaku: ■■ „ASCII” tablica obiektów typu char (8 bitowe); ■■ L”wchar_t” tablica obiektów wchar_t (16 bitowe); ■■ u8”UTF-8 String \u03CO” tablica obiektów typu char, kodowanie UTF-8, znak UTF 03C0 - to pi; ■■ u”UTF-16 String \u03C0” tablica obiektów char16_t (16 bitowe), kodowanie UTF-16; ■■ U”UTF-32 String \u03C0”. tablica obiektów char32_t (32 bitowe), kodowanie UTF-32. Tablice znaków są zakończone znakiem końca napisu, tzn. znakiem \0, jest to standard stosowany w C. Tablicę znaków można utworzyć za pomocą metody c_str() dla obiektu std::string, można także użyć takiej tablicy do inicjacji obiektu std::string. Wyrażenia regularne pozwalają opisywać grupę „po- dobnych” napisów. Opisy takie są przydatne, gdy chcemy wykonywać pewne operacje na napisach, które reprezen- tują pewien szablon. Wyrażenia regularne są przydatne przy przetwarzaniu tekstów, dlatego są dostępne stan- dardowo w tych językach skryptowych. Zostały także dodane do biblioteki standardowej w nowej wersji stan- dardu C++11, oferują ją m.in. kompilatory Visual Studio 2010 oraz GNU GCC 4.6. Dla innych, stosowanych obec- nie kompilatorów, możemy wykorzystać jedną z kilku bi- bliotek boost. Wyrażenia regularne można opisać napisem, który zawiera symbole specjalne, patrz ramka. Przykładowo: .a oznacza dwuliterowy napis kończący się literą a (1a, aa, Aa, ...); [AEO]la oznacza napis Ala, Ela lub Ola; \da albo [[:digit:]]a oznacza dwuliterowy napis rozpoczynający się cyfrą, a kończący literą a (1a, 2a, ... ,9a); a*b oznacza dowolną liczbę liter a, a następnie literę b (b, ab, aab, ...); a? b oznacza opcjonalne wystąpienie litery a, a następnie literę b, czyli opisuje dwa napisy: b i ab; (a|b)b oznacza wystąpienie litery a lub b na pierwszej pozycji, a następnie litery b; (ab)+ oznacza przynajmniej jedno powtórzenie ciągu ab (ab, abab, ababab, ...); a(a|b)*b oznacza wyra- żenie regularne, które opisuje napisy rozpoczynające się literą a, zakończone literą b, składające się jedynie z liter a i b (napisy ab, aab, abb, aaab, aabb, abab, abbb, ...).

7/ www.programistamag.pl / WYRAŻENIA REGULARNE W C++ ZNAKI PISARSKIE są kodowane w sposób standardowy od XIX wieku, używano wtedy kodu Morsa oraz ko- dów telegraficznych (5 bitów na znak). W systemach komputerowych domi- nującą rolę odgrywa ASCII (American Standard Code for Information Inter- change, 7 bitowy), a ostatnio Unicode (znaki 16- i 32 bitowe oraz o zmiennej długości od 8 do 32 bitów). Taka różnorodność wynika z faktu istnienia wielu alfabetów narodowych i wielu metod ich obsługi. Jeżeli wyko- rzystujemy ASCII, 7 bitów jest wykorzy- stywane na kodowanie znaków alfabetu łacińskiego, używanych w tekstach utworzonych w języku angielskim, cyfr i znaków przestankowych. Do repre- zentacji innych znaków, na przykład znaków narodowych wykorzystano ósmy bit, co daje możliwość zakodowa- nia dodatkowych 128 symboli. Niestety, dodatkowych znaków, które chcieliby- śmy zakodować jest znacznie więcej niż 128, więc istnieje wiele standardowych zbiorów konwertujących 8 bitów na znak pisarski, zbiór taki nazywamy stroną kodową. Reprezentacja 8 bitowa oraz mechanizm stron kodowych, pozwala reprezentować teksty w wię­ kszości używanych języków. Niestety takie kodowanie nie pozwala, w prosty sposób, umieszczać w tym samym tekście znaków z różnych stron kodo- wych, więc jeżeli tekst zawiera napisy w dwóch różnych językach jest kłopo- tliwy do kodowania. Z drugiej strony, te same ciągi bajtów mogą reprezentować różne ciągi znaków pisarskich, co może prowadzić do trudności z odczytaniem tekstu. Wymienione problemy rozwiąza- no, stosując dłuższe, niż 1-bajtowe, reprezentacje znaków. Znaki 16 bitowe (UTF16, UCS2) pozwalają zakodować symbole używane w większości współ- czesnych języków, znaki 32 bitowe (UTF32, UCS-4) pozwalają kodować wszystkie symbole, także występujące w alfabetach obecnie nie stosowanych (np. hieroglify).Wadą napisów wykorzy- stujących kodowanie szerokimi znaka- mi, jest zajętość pamięci, gdy kodujemy napisy w języku angielskim.Wadę tę można eliminować, stosując kodowanie znaków przez obiekty o różnej długości: znaki ASCII przez symbole 1-bajtowe (8 bitów), znaki z alfabetów narodo- wych przez symbole dłuższe (2, 3 lub 4 bajty).Takie kodowanie jest opisane przez standard UTF8. Zaletą UTF8 jest zgodność z ASCII, wadą – niebanalne obliczanie długości napisu, nie ma pro- stej zależności liczby znaków w napisie od liczby bajtów zajmowanych przez napis. Obecnie najbardziej popularne jest kodowanie UTF-8. Język C++ wspiera wszystkie wymienione wyżej standardy. Wybrane na potrzeby demonstracji symbole specjalne sto- sowane przy opisie wyrażeń regularnych. Pełna lista (kilka- dziesiąt symboli) jest dostępna w dokumentacji bibliotek. Symbol Opis ^ $ początek i koniec linii . dowolny pojedynczy znak [aeo] zbiór znaków [a-z] zakres znaków [^0-9] znak spoza zakresu [[:alpha:]] litera (predefiniowany zbiór znaków) [[:digit:]] cyfra (predefiniowany zbiór znaków) [[:space:]] biały znak (predefiniowany zbiór znaków) \d to samo co [[:digit:]] \s to samo co [[:space:]] * dowolna liczba (zero lub więcej) powtórzeń poprzed- niego symbolu + jedno lub więcej powtórzenie poprzedniego symbolu ? opcjonalność: zero lub jedno powtórzenie {m,n} od m do n wystąpień (wyr) grupa, do której odnoszą się inne operacje | alternatywa Za pomocą przedstawionego opisu można utworzyć obiekt reprezentujący wyrażenie regularne. Obiekt taki pozwala typowo na następujące operacje: ■■ badanie, czy dany napis jest opisywany przez wyraże- nie regularne; ■■ badanie, czy dany napis zawiera ciąg symboli (pod-napis) opisywany przez wyrażenie regularne; ■■ zamiana ciągu symboli opisywanych wyrażeniem regu- larnym na inny ciąg symboli. Dodatkowo dostępne są metody iteracji po odnalezionych pod-napisach opisywanych wyrażeniem regularnym oraz dzielenia tekstu na grupy. W artykule będziemy wykorzystywali bibliotekę bo- ost::regex, która jest dostępna od kilku lat dla wielu platform i jest w dużej mierze zgodna ze standardem. Dodatkowo opisana będzie biblioteka boost::xpressive, która pozwala tworzyć obiekty reprezentujące wyrażenia regularne w czasie kompilacji. Obiekty reprezentujące wyrażenie regularne przedstawione w tym artykule mają semantykę wartości, można je kopiować, przekazywać jako argument czy zwracać z funkcji lub metody. BIBLIOTEKA BOOST::REGEXP Wyrażenie regularne jest obiektem typu szablonowego basic_regex, gdzie parametrem jest typ znaku. Dla zna- ków char klasa ta nazywa się regex, dla wchar_t – wre- gex. Napis opisujący wyrażenie regularne jest przekazy- wany w konstruktorze, gdzie tworzony jest wewnętrzna reprezentacja wyrażenia regularnego (jest nią automat skończony). Jeżeli obiekt jest prawidłowo zainicjowany, co oznacza m.in., że opis jest poprawny, to metoda basic_ regex::empty() zwraca false. Opis wyrażenia regularnego to napis zawierający symbole specjalne, tak jak pokaza- no wcześniej. Jeżeli symbol posiadający specjalne zna- czenie chcemy wykorzystać wprost, stawiamy przed nim lewy ukośnik (backslash, \). Proszę zwrócić uwagę, że kasowanie specjalnego znaczenia symbolu w wyrażeniu regularnym odbywa się za pomocą tego samego znaku, co kasowanie specjalnego znaczenia wewnątrz stałych napisowych języka C++, więc jeżeli wyrażenie regularne

8 / 1 . 2012 . (1)  / BIBLIOTEKI I NARZĘDZIA jest dostarczone jako stała napisowa (napis w cudzysło- wach umieszczony w kodzie), to musimy backslash pisać podwójnie! Po poprawnym utworzeniu obiektu reprezentującego dane wyrażenie regularne możemy używać funkcji opi- sanych poniżej. Badanie, czy napis jest opisywany wy- rażeniem regularnym jest realizowane przez funkcję re- gex_match, wyszukiwanie ciągu znaków (pod-napisu) opisywanego danym wyrażeniem wykonuje regex_se- arch, regex_replace pozwala zastępować fragmenty opi- sane danym wyrażeniem regularnym innym napisem. Funkcja regex_match wymaga podania dwóch argu- mentów: napisu który będzie badany oraz obiektu repre- zentującego wyrażenie regularne. Zwraca ona wartość logiczną, czy napis jest opisywany wyrażeniem regular- nym. Przykład użycia tej funkcji zawiera Listing 1, gdzie pokazano funkcję correctIP sprawdzającą, czy napis jest poprawnym adresem IP. Przedstawiony kod sprowadza badanie tej poprawności do badania warunku, czy na- pis składa się z 4 sekcji zawierających od 1 do 3 cyfr i rozdzielonych kropką, nie jest to badanie doskonałe, za poprawny adres IP zostaną uznane napisy takie jak 256.0.0.0 czy 0.0.0.999. Wyrażenia regularne pozwalają taką funkcję zapisać za pomocą dwóch linii kodu. Funkcja regex_search bada, czy dany napis zawiera ciąg symboli (pod-napis) opisywany przez wyrażenie re- gularne, zwraca ona prawdę logiczną (true), jeżeli taki ciąg udało się odnaleźć. Istnieje możliwość uzyskania wskazania na ten pasujący pod-napis, obiekt typu smatch, przekazywany jako argument referencyjny do funkcji re- Pierwszy element kolekcji wskazuje na ciąg opisywa- ny całym wyrażeniem regularnym, drugi element kolekcji (element o indeksie 1) opisuje ciąg „pasujący” do pierw- szej grupy (grupy w wyrażeniu regularnym tworzone są za pomocą nawiasów), trzeci element kolekcji to ciąg od- powiadający drugiej grupie itd. bool correctIP(const std::string& ip) { static const regex e("\\d{1,3}\\.\\d{1,3}\\.\\ d{1,3}\\.\\d{1,3}"); return regex_match(ip, e); } Listing 1. Badanie, czy napis opisuje poprawny adres IP Listing 2. Wyszukiwanie wzorca opisywanego wyrażeniem regu- larnym. Tutaj kod, który przeszukuje tekst html w poszukiwaniu adresów e-mail gex_search jest wypełniany odpowiednimi wartościami. Przykład zawiera Listing 2, który pokazuje kod wyszuku- jący odpowiedni znacznik html w łańcuchu znaków. Obiekty generowane przez szablon match_result, za- wierające kolekcję par wskaźników lub par iteratorów, są wykorzystywane do wskazywania na ciąg znaków. Pierw- szy wskaźnik (lub pierwszy iterator) z pary wskazuje na pierwszy znak opisywanego ciągu, zaś drugi – na znak następny po ostatnim znaku opisywanego ciągu (stosuje się tutaj konwencję taką, jak dla zakresu iteratorów w bi- bliotece standardowej C++). Biblioteka definiuje cztery różne typy, generowane na podstawie tego szablonu (róż- ne typy znaków, wskaźnik lub iterator): ■■ typedef match_results cmatch; ■■ typedef match_results wcmatch; ■■ typedef match_results smatch; ■■ typedef match_results wsmatch. string html = /* wczytaj stronę html */; regex mail("href=\"mailto:(.*?)\">"); smatch what;//przechowuje wyniki wyszukiwania if( regex_search(html,what,mail) ) { //what[0] napis pasujący do wyrażenia cout << "mail to" << what[1] << endl; } W przykładzie pokazanym na Listingu 2 wykorzystuje- my niezachłanne dopasowywanie do grupy, co jest zapi- sane w wyrażeniu regularnym jako (.*? ). Symbole wielo- znaczne, na przykład *, mogą reprezentować pod-napisy różnej długości, co pozwala na powstawanie przypadków, gdy ten sam napis wejściowy może być interpretowany na wiele sposobów. Biblioteka boost::regex wykorzystuje dwa rodzaje dopasowań: zachłanne, gdy symbol opisuje pod-napis o maksymalnej długości, oraz niezachłanne, gdy dopasowanie oznacza ciąg o minimalnej długości. W ten sposób usuwane są niejednoznaczności. Wyraże- nie regularne <(.*)> wskaże na ciąg

Hello

dla wejścia xxx

Hello

yyy, zaś wyrażenie <(.*? )>znajdzie

dla tego samego wejścia. Funkcja regex_replace pozwala zastępować fragmen- ty opisane danym wyrażeniem regularnym wybranym napisem. Argumentami tej funkcji są: napis wejściowy, wyrażenie regularne oraz napis formatujący, czyli napis, który będzie wstawiany w miejsce ciągu opisanego wy- rażeniem regularnym. Napis formatujący może zawierać kilka symboli specjalnych, między innymi znak &, który określa fragment tekstu opisany wyrażeniem regular- nym; znak \D oznacza fragment tekstu dopasowany do Operatory reprezentujące niektóre symbole specjalne dla boost::xpressive wyrażenie typowy symbol opis bos ^ początek sekwencji a >> b brak konkatenacja as_xpr(’a’) a znak as_xpr(”abc”) abc ciąg znaków (set=’a’,’b’,’c’) [abc] zbiór zbiór _ . dowolny znak | | alternatywa * * dowolna liczba wystąpień, - operator jest prefiksowy + + jedno lub więcej wystąpienie ? ! opcjonalność, prefiksowy (s1= a), gdzie 1 to nr grupy (a) grupa

9/ www.programistamag.pl / WYRAŻENIA REGULARNE W C++ string subject(const string& in) { smatch w; //pod-napisy dopasowane do grup if( regex_match(in, w, bos>>"Subject: " >>(s1=*(as_xpr("Re: ")|as_xpr("Odp: "))) >>(s2=*_) ) ) { return string(w[2].first, w[2].second); } return ””; } grupy D, gdzie D = 1, 2, ..., 9. Przykładowo regex_re- place("(

)+", "

hello", "

") zwróci napis

hello, zaś regex_replace("a,bx", reg, "\\2 = \\1") zwróci napis b = ax. BIBLIOTEKA BOOST::XPRESSIVE - WYRAŻENIA W CZASIE KOMPILACJI Biblioteka boost::xpressive jest inną biblioteką, wchodzą- cą w skład zbioru boost, która dostarcza obiektów i funk- cji pozwalających wykorzystywać wyrażenia regularne. Oferuje ona udogodnienia takie jak opisane poprzednio, a dodatkowo pozwala tworzyć obiekty reprezentujące wyrażenie regularne w czasie kompilacji. Biblioteka bo- ost::regex umożliwia jedynie zaszycie napisu opisującego wyrażenie w kodzie, automat skończony (reprezentacja wyrażenia regularnego) jest tworzony w konstruktorze tego obiektu - czyli w czasie działania. Tworzenie repre- zentacji wyrażenia regularnego w czasie kompilacji po- zwala zaoszczędzić czas podczas działania aplikacji, nie trzeba wtedy przetwarzać napisu opisującego wyrażenie i budować reprezentacji wewnętrznej. Budowa obiektu reprezentującego wyrażenie regularne wykorzystuje techniki związane z meta-programowaniem, opisane m.in. w książce Nowak, Pająk „Język C++: me- chanizmy, wzorce, biblioteki”. Obiekt taki jest tworzony na podstawie wyrażenia, a nie na podstawie napisu. Wyraże- nie to wykorzystuje obiekty klas dostarczanych przez bi- bliotekę, dla których przeciążono operatory reprezentujące symbole specjalne używane w wyrażeniach regularnych. Opis wyrażenia regularnego jest wyrażeniem w C++, co wymusza stosowanie innych, niż typowe, symboli w nie- których przypadkach. Podstawowe elementy takich wyra- żeń zostały przedstawione w ramce, pozostałych kilkadzie- siąt jest opisanych w dokumentacji biblioteki. Przy tworzeniu opisu wyrażenia regularnego za pomo- cą wyrażeń C++ jesteśmy zmuszeni zapisywać symbol konkatenacji (łączenia kolejnych części opisujących wy- rażenie regularne), czyli stosować operator >>. Opisując wyrażenie regularne napisem nie było takiej konieczno- ści, napis ^a oznaczał wystąpienie litery a na początku linii, tutaj musimy to zapisać jako bos >> a, gdzie bos oznacza obiekt (stałą) reprezentujący początek napisu. Listing 3. Funkcja dostarcza tytuł listu, pomijając przedrostki doda- wane przez programy pocztowe Znak lub ciąg znaków zapisujemy jako wynik działania funkcji as_xpr, operatory działają na obiektach pewnych typów, a nie na stałych znakowych i stałych napisowych. Jeżeli kompilator jest w stanie jednoznacznie zastosować konwersję, to możemy pominąć zapis as_xpr('a') i pisać 'a'. Dowolny znak nie jest oznaczany kropką, a podkre­ ślnikiem, więc _ >> ’a’ opisuje dwuliterowe napisy koń- czące się literą a; operator * i + jest prefiksowy, więc * as_xpr(’a’) >> ’b’ oznacza dowolną liczbę liter a, a nastę­ pnie literę b (b, ab, aab, ...); opcjonalne wystąpienie sym- bolu (lub grupy) zapisujemy jako ! , więc !as_xpr(’a’) >>’b’ oznacza opcjonalne wystąpienie litery a, a nastę­pnie literę b, czyli opisuje dwa napisy: b i ab. Wyrażenie bos >> "Subject: " >> (s1=*(as_xpr("Re: ")|as_xpr("Odp: "))) >> (s2=*_) wyszukuje w treści listu (e-mail) tytułu, pomijając przedrostki dodawane przez programy poczto- we. Identyczny obiekt można reprezentować napisem: Subject: (Re: |Odp: )*(.*). Wyrażenia takiego użyto na Listingu 3. Funkcja subject zwróci napis witajcie dla wej- ścia Subject: Re: Re: Odp: witajcie. PODSUMOWANIE Biblioteka boost::regex jest dostarczana jako biblioteka binarna, trzeba ją konsolidować. Biblioteka boost::xpres- sive zawiera tylko nagłówki, do jej użycia nie jest po- trzebna konsolidacja. Wyrażenia regularne (takie jak boost::regex) są już dostępne dla popularnych kompila- torów, a będą powszechne, ponieważ są częścią biblioteki standardowej języka (standard C++11). W Sieci PP http://www.boost.org – dokumentacja bibliotek boost, PP http://www.open-std.org/jtc1/sc22/wg21/docs/pa- pers/2011/n3242.pdf – standard C++11 (working draft). Więcej w książce Omówienie współcześnie stosowanych technik, wzorce projektowe, programowanie generyczne, prawidłowe zarzą- dzanie zasobami przy stosowaniu wyjątków, programowa- nie wielowątkowe, ilustrowane przykładami stosowanymi w bibliotece standardowej i bibliotekach boost, opisano w książce Robert Nowak, Andrzej Pająk ,,Język C++: mecha- nizmy, wzorce, biblioteki’’, BTC 2010. Robert Nowak rno@o2.pl Adiunkt w Zakładzie Sztucznej Inteligencji Instytutu Systemów Elektronicznych Po- litechniki Warszawskiej, zainteresowany tworzeniem aplikacji wykorzystujących al- gorytmy sztucznej inteligencji i fuzji danych. Autor biblioteki faif.sourceforge.net. Programuje w C++ od ponad 15 lat.

10 / 1 . 2012 . (1)  / PROGRAMOWANIE GRAFIKI Paweł Wolniewicz P owstanie algorytmów i bibliotek umożliwiających rozpoznawanie obiektów na obrazach pozwoliło między innymi na stworzenie aplikacji służących do wykrywania twarzy, przeszkód, dowolnych kształtów o charakterystyce zbliżonej do zadanej przez programistę. Początkowo rozwiązania takie były używane w robotyce, biometrii oraz innych zastosowaniach przemysłowych. Obecnie z komputerową analizą obrazu ma styczność każdy posiadacz kamery cyfrowej wykrywającej ludzkie twarze, użytkownik oferującej identyczne funkcje usługi Google Picasa, a także wielu innych aplikacji. Nowe możliwości otworzyło pojawienie się smartfonów wyposażonych w aparaty pozwalające na wykonywanie zdjęć w przyzwoitej jakości. Powstał nowy rynek aplikacji, które nie miały racji bytu w przypadku komputerów sta- cjonarnych, a nawet laptopów. Przykładowe zastosowania to między innymi: ■■ wykrywanie i rozpoznawanie budynków na obrazie z kamery (na potrzeby wzbogaconej rzeczywistości – augmented reality), ■■ odnajdywanie twarzy na obrazie jeszcze przed wyko- naniem zdjęcia, w celu poprawienia jakości wykonywa- nych fotografii, ■■ korekcja zniekształceń wynikających z fotografowania płaskich powierzchni pod kątem (co ułatwia wykorzy- stanie smartfonu w roli skanera dokumentów). Wszystkie wymienione przykłady wymagają opracowania kodu wykrywającego na obrazie krawędzie, linie lub figu- ry geometryczne o określonym kształcie, a także obiekty o dużym stopniu komplikacji. Nie wystarczy tutaj użycie filtrów, progowania bądź binaryzacji. Trzeba sięgnąć po metody umożliwiające precyzyjne wskazanie na obrazie konkretnych obiektów. WYBÓR BIBLIOTEK Rozpocznijmy od przygotowania środowiska pracy. Pisa- nie od podstaw kodu służącego do analizowania obrazu mija się z celem. Istnieją gotowe biblioteki pozwalające zarówno na przygotowanie grafiki źródłowej, jak i wydo- bycie z odpowiednio przetworzonych zdjęć i klatek filmu interesujących nas obiektów. Istnieją pakiety narzędzia programistyczne przezna- czone dla konkretnych platform. Przykładowo, progra- miści systemu iOS mogą wykorzystać niewielką klasę simple-iphone-image-processing. Zawiera ona funkcje pozwalające na binaryzację grafiki, wykrywanie krawę- dzi, rozmywanie, manipulowanie jasnością, a także na wykrywanie obiektów w obrazach binarnych. Nie jest to wiele – w bardziej zaawansowanych zastosowaniach trzeba pisać własny kod lub sięgnąć po dodatkowe na- rzędzia. Bardziej sensownym rozwiązaniem jest skorzystanie z rozbudowanych bibliotek, oferujących pełny pakiet na- rzędzi do analizy obrazu – od pobrania go z kamery, przez operacje na kanałach i pojedynczych pikselach, progowanie, aż po tworzenie bazy wykrytych konturów i przedmiotów. Jednym z takich uniwersalnych narzędzi jest OpenCV (Open Source Computer Vision). Bibliote- ka ta jest rozpowszechniana na warunkach licencji BSD i zawiera ponad 2,5 tysiąca zoptymalizowanych algory­ tmów do przetwarzania obrazu. Olbrzymią zaletą OpenCV jest jej wieloplatformowość. Biblioteka działa z powodzeniem zarówno na urządze- niach stacjonarnych (wspierane systemy to Windows i Unix), jak i na smartfonach oraz tabletach. Oprócz obsługi Androida OpenCV doczekało się także wersji przeznaczonej dla iOS. Niewykluczone, że w przyszłości otrzymamy również wsparcie dla kolejnych systemów operacyjnych dla urządzeń mobilnych. Kod źródłowy przedstawiony w dalszej części artykułu przeznaczony będzie dla OpenCV wykorzystywanego na komputerach stacjonarnych i laptopach. Porty biblioteki dla systemów mobilnych znajdują się na razie w fazie eksperymentalnej, stąd też korzystanie z nich jest nie- co trudniejsze. Użytkownicy zainteresowani tworzeniem aplikacji dla iPhone oraz iPada powinni przejrzeć krótki samouczek (w języku angielskim), opisujący pierwsze kroki z pakietem. O ile same funkcje i struktury danych nie różnią się bez względu na platformę, o tyle instala- cja biblioteki oraz sposób odwoływania się do plików na- główkowych zależy od wykorzystywanego systemu dla urządzeń mobilnych. W przypadku Androida warto zapo- znać się z podrozdziałem dokumentacji prezentującym sposób użycia biblioteki w środowisku Eclipse. Funkcja programowego wykrywania i rozpoznawania obiektów znajdujących się na obrazach oraz klatkach sekwencji wideo znajduje zastosowanie w wielu aplikacjach. Dzisiaj oprogramowanie komputerowe nie ogranicza się bowiem do katalogowania, wyświetlania i edycji grafiki, ale analizuje również zawartość zdjęć i filmów. Analiza obrazu: rozpoznawanie obiektów

11/ www.programistamag.pl / ANALIZA OBRAZU: ROZPOZNAWANIE OBIEKTÓW Wykorzystanie OpenCV w celu tworzenia oprogramo- wania dla komputerów stacjonarnych i laptopów jest najłatwiejsze i sprowadza się do pobrania pakietu dla systemu Windows lub zainstalowania paczek w dowolnej z popularnych dystrybucji Linuksa. W przypadku syste- mu Mac OS X sytuacja jest trudniejsza. Należy przede wszystkim odwiedzić stronę internetową pakietu na wi- trynie OpenCV. Znajdziemy tam podstawowe informacje instalacyjne oraz link do plików do pobrania. Bibliotekę OpenCV możemy użyć niezależnie od wyko- rzystywanego przez nas środowiska programistycznego. Dla uproszczenia będziemy przygotowywali kod gotowy do skompilowania za pomocą jednej komendy dowolnego kompilatora dostępnego na licencji open source, na przy- kład MinGW (w Windows) i GCC (w Linuksie). Oprócz OpenCV można skorzystać także z innych bi- bliotek o podobnym zastosowaniu. Warto wypróbować zwłaszcza ITK (Insight Segmentation and Registration Toolkit). Tak jak OpenCV, biblioteka została napisana w ję- zyku C++. Można wykorzystać ją jednak także podczas tworzenia aplikacji w innych językach, między innymi Java. ITK jest biblioteką wszechstronną. Olbrzymia lista klas zawiera narzędzia przeznaczone zarówno do wyko- nywania operacji na pojedynczych pikselach, korzystania z dziesiątek (setek?) filtrów, jak i przetwarzania konturów wykrytych na analizowanych obrazach. Pod względem funkcjonalności jest to najlepsza alternatywa na licencji open source dla biblioteki OpenCV. Problem stanowi je­ dnak wsparcie dla systemów operacyjnych dla urządzeń mobilnych. Pod tym względem o wiele lepiej prezentuje się OpenCV. Jeśli więc zamierzamy tworzyć oprogramo- wanie, które w przyszłości trafi do smartfonów i tabletów, to dobrym rozwiązaniem będzie wybór OpenCV. ZALĄŻEK PROJEKTU Aplikacja rozpoznająca obiekty na zdjęciach / klipach wi- deo powinna wykonywać w tle następujące zadania: ■■ pobieranie obrazu ze źródła (kamera cyfrowa, plik na dysku), ■■ przetwarzanie uzyskanego obraz w taki sposób, by możliwe było bezbłędne wyodrębnienie potrzebnych obiektów, ■■ wykrywanie obiektów, ■■ wykonywanie operacji na wykrytych konturach / przed- miotach. Niezależnie od tego powinniśmy również przygotować in- terfejs, za pomocą którego aplikacja będzie komunikowa- ła się z użytkownikiem i przedstawiała mu efekty swojej pracy. Wszystko zależy od preferencji programisty i jego indywidualnych potrzeb. Jeśli decydujemy się na stoso- wanie przenośnych bibliotek open source, to graficzny interfejs może korzystać na przykład z Qt. Wystarczy do- konać konwersji pomiędzy obiektami IplImage i QImage, by uzyskać z poziomu Qt dostęp do funkcji oferowanych przez OpenCV. Potrzebny kod można znaleźć na stronach Nokii (właściciela Qt). Powinniśmy skopiować i włączyć do projektu funkcję IplImage2QImage. Jest ona odpo- wiedzialna za konwersję obrazów OpenCV do postaci akceptowalnej przez bibliotekę Qt. Dodatkowa funkcja qImage2IplImage działa w sposób odwrotny. Jeżeli pro- ces tworzenia obrazu QImage zakończy się niepowodze- niem, to konieczna będzie konwersja przestrzeni barw z poziomu OpenCV (funkcja cvCvtColor). POZYSKANIE OBRAZU Tworzenie własnej aplikacji rozpoznającej obiekty znajdu- jące się na zdjęciach oraz klatkach sekwencji wideo mu- simy rozpocząć od pobrania obrazu źródłowego. W tym celu napiszemy prosty kod w języku C++. Uzyska on do- stęp do kamery cyfrowej, pobierze kolejne klatki obrazu, a następnie wyświetli go na ekranie w osobnym oknie. Na razie nie będziemy stosowali żadnych funkcji służących do analizy obrazu ani szukania obiektów. Zbudujemy tyl- ko szkielet aplikacji wykorzystującej OpenCV. Przykładowy kod zaprezentowany został na listingu 1. Za połączenie z kamerą internetową podłączoną do portu #include #include #include int main() { // pobranie obrazu z dowolnej kamery CvCapture* obraz = cvCaptureFromCAM (CV_ CAP_ANY); if ( !obraz ) { fprintf( stderr, "Brak kamery\n" ); return -1; } // przygotowanie okienka cvNamedWindow( "okienko", CV_WINDOW_ AUTOSIZE ); // pobieranie kolejnych klatek aż do naciśnięcia klawisza ESC while ( 1 ) { IplImage* klatka = cvQueryFrame( obraz ); if ( !klatka ) { fprintf( stderr, "Brak klatki\n" ); break; } // wyświetlanie klatek w okienku cvShowImage( "okienko", klatka ); if ( ( cvWaitKey(10) & 255 ) == 27 ) break; } // porządkowanie cvReleaseCapture( &obraz ); cvDestroyWindow( "okienko" ); return 0; } Listing 1. Pobieranie obrazu z kamery cyfrowej

12 / 1 . 2012 . (1)  / PROGRAMOWANIE GRAFIKI Rysunek 1. Oryginalny obraz wykorzystany w testach Rysunek 2. Efekt dwukrotnego przeskalowania oryginalnego obrazu Rysunek 3. Efekt binaryzacji z użyciem funkcji cvThreshold Rysunek 4.Tak działa detekcja krawędzi metodą Canny’ego Rysunek 5. Pułapki binaryzacji i detekcji krawędzi: źle dobrane progi w połączeniu z niewielkim kontrastem pomiędzy kartką papieru a jej tłem spowodowały, że brzegi papieru nie zostały uchwycone przez zastosowaną w dalszej kolejności metodę transformacji Hougha Rysunek 6. Okręgi odnalezione przez funkcję cvHoughCircles Rysunek 7. Efekt działania algorytmu szukającego prostokątów. USB odpowiedzialna jest funkcja cvCaptureFromCAM. Łą- czymy się z dowolnym urządzeniem z obecnych w syste- mie (parametr CV_CAP_ANY). Kolejne klatki pobierane są za pomocą funkcji cvQueryFrame wykonywanej wewnątrz pętli while. I wreszcie, linijki zaczynające się od cvName- dWindow oraz cvShowImage przygotowują okienko oraz wypełniają je treścią, czyli kolejnymi klatkami. Jeszcze łatwiejszym zadaniem jest wyświetlenie obra- zu z pliku. Kod kompletnego programu ładującego grafikę z dysku ma zaledwie 20 linijek (listing 2). Jedyną nowość stanowi funkcja cvLoadImage, z parametrem w postaci ścieżki dostępu. Jest ona odpowiedzialna za otwieranie obrazu.

13/ www.programistamag.pl / ANALIZA OBRAZU: ROZPOZNAWANIE OBIEKTÓW #include #include #include int main() { IplImage* obraz = 0; // wskazujemy nazwę pliku obraz=cvLoadImage("obrazek.tif"); if(!obraz){ printf("Problem z obrazkiem!"); exit(0); } // wyświetlanie obrazu z pliku cvNamedWindow("okienko", CV_WINDOW_ AUTOSIZE); cvShowImage("okienko", obraz ); cvWaitKey(0); // porządki cvReleaseImage(&obraz ); return 0; } Listing 2. Otwieranie obrazu z dysku PRZYGOTOWANIE OBRAZU DO ANALIZY Zdjęcie z dysku lub obraz z kamery zazwyczaj muszą być odpowiednio przygotowane do analiz polegających na rozpoznawaniu obiektów. W przeciwnym wypadku uzy- skane rezultaty będą dalekie od oczekiwań. Niezbędne operacje można podzielić na kilka grup: ■■ usuwanie z obrazu artefaktów mogących zakłócić ana- lizy, ■■ konwersję do odcieni szarości i/lub binaryzację, ■■ wykrywanie krawędzi. Nie wszystkie te etapy należy wykonać. Zabiegiem po- trzebnym najczęściej jest poprawianie czytelności źró- dłowego obrazu. W tym celu konieczne jest skorzystanie z filtrów lub zastosowanie prostego sposobu polegającego na zmniejszeniu obrazu, a następnie przeskalowaniu do pierwotnej rozdzielczości. Drobne artefakty zostaną wów- czas przynajmniej częściowo usunięte na skutek interpo- lacji. Rozpocznijmy od techniki polegającej na dwukrotnym skalowaniu obrazu. Poprzedzimy ją konwersją klatki wi- deo do odcieni szarości. Większość algorytmów, które wykorzystamy później, i tak operuje na pojedynczym ka- nale. Konwersja i skalowanie grafiki zaprezentowane są na listingu 3. Za konwersję obrazu do innej przestrzeni barw w OpenCV odpowiedzialna jest funkcja cvCvtColor. Po wskazaniu grafiki źródłowej oraz docelowej określamy jeszcze rodzaj konwersji. Aby uzyskać obraz w odcieniach szarości należy ustalić wartość tego parametru jako CV_ BGR2GRAY. Skalowanie obrazu umożliwiają natomiast funkcje cvPyrDown i cvPyrUp. Wcześniej warto jeszcze ograniczyć rozmiar grafiki, jeśli szerokość lub wysokość #include #include #include int main() { CvCapture* obraz = cvCaptureFromCAM (CV_ CAP_ANY); if ( !obraz ) { fprintf( stderr, "Brak kamery\n" ); return -1; } cvNamedWindow( "okienko", CV_WINDOW_ AUTOSIZE ); // zmienna przechowująca rozmiar oryginalnego obrazu, po zaokrągleniu w przypadku, gdy szerokość i/lub wysokość są wartościami nieparzystymi CvSize parzysty; while ( 1 ) { IplImage* klatka = cvQueryFrame( obraz ); if ( !klatka ) { fprintf( stderr, "Brak klatki\n" ); break; } parzysty = cvSize( klatka->width & -2, klatka->height & -2 ); IplImage* szary = cvCreateImage( parzysty, IPL_DEPTH_8U, 1 ); IplImage* maly = cvCreateImage( cvSize(parzysty.width/2, parzysty.height/2), IPL_DEPTH_8U, 1 ); // ograniczenie dalszych operacji do obszaru grafiki o rozmiarach będących liczbami podzielnymi przez 2 cvSetImageROI( klatka, cvRect( 0, 0, parzysty.width, parzysty.height )); // konwersja do odcieni szarości cvCvtColor(klatka, szary, CV_BGR2GRAY); // skalowanie obrazu "w dół" cvPyrDown( szary, maly, 7 ); // skalowanie obrazu "w górę" cvPyrUp( maly, szary, 7 ); cvShowImage( "okienko", szary ); cvReleaseImage( &maly ); cvReleaseImage( &szary ); if ( ( cvWaitKey(10) & 255 ) == 27 ) break; } cvReleaseCapture( &obraz ); cvDestroyWindow( "okienko" ); return 0; } Listing 3. Skalowanie obrazu i konwersja przestrzeni barw w OpenCV

14 / 1 . 2012 . (1)  / PROGRAMOWANIE GRAFIKI mają wartości nieparzyste. Dokonujemy tego funkcją cvSetImageROI. Zamiast skalować obraz można skorzystać z filtrów, które wygładzą źródłową grafikę, likwidując drobne za- kłócenia. W tym celu użyjemy funkcji cvSmooth. Do wy- boru mamy kilka filtrów. Jeżeli obraz źródłowy powinien być rozmyty, to dobrym rozwiązaniem będzie filtr Gaus- sa. Aby z niego skorzystać, należy określić wartość para- metru smoothtype jako CV_GAUSSIAN. Niewielkie zakłócenia na obrazie, które mogą utrudnić proces rozpoznawania obiektów, można usunąć za po- mocą filtra medianowego. Zastępuje on wartości pikse- li medianą z wartości sąsiednich punktów. Likwiduje to szum w niewielkiej skali, zachowując ogólne tendencje, dzięki czemu krawędzie nie są niszczone. Umożliwia to późniejszą detekcję obiektów w obrazie. Filtr medianowy stosujemy za pomocą funkcji cvSmooth, przy czym war- tość parametru smoothtype powinna być określona jako CV_MEDIAN. Wszystkie narzędzia wchodzące w skład OpenCV i słu- żące do usuwania zakłóceń oraz wygładzania obrazu opi- sane są w dokumentacji biblioteki. Użytkownik korzysta z gotowych rozwiązań oferowanych przez funkcję cvSmo- oth lub przygotowuje własny filtr konwolucyjny, definiując uprzednio jego maskę (cvFilter2D). Usuwanie niedoskonałości z pozyskanego obrazu ma ogromne znaczenie dla rozpoznawania obiektów. Wię­ kszość analiz tego typu oparta jest bowiem na wykry- waniu w badanej grafice wyraźnych krawędzi, nierzadko mających charakter prostych linii bądź okręgów. Artefak- ty mogą zatrzeć takie granice. Duża rozdzielczość powo- duje ponadto, że istotne krawędzie zostają rozmyte do poziomu słabych gradientów. Poza tym ich przebieg jest zakłócony przez liczne, drobne elementy obrazu, które w niskiej rozdzielczości zostałyby po prostu pominięte. Ważnym problemem wpływającym na proces przygoto- wania obrazu są także odbiegające od optymalnych usta- wienia kontrastu i jasności. Mogą one spowodować, że wyraźna krawędź stanie się mało widoczna – i w efe­kcie pominięta przez algorytmy odnajdujące kształty. Prosta zmiana parametrów kamery może niekiedy dopomóc wię- cej niż skomplikowane manipulacje z użyciem filtrów. BINARYZACJA I WYKRYWANIE KRAWĘDZI Po wstępnym przygotowaniu obrazu można go zbinary- zować, o ile oczywiście jest to wymagane w kolejnych etapach pracy nad grafiką lub klatką filmową. Binaryzacja polega na ograniczeniu przestrzeni barw do czerni i bieli. Każdy piksel jest w efekcie opisany jednym bitem danych. Binaryzacja ma z reguły na celu oddzielenie istotnych ele- mentów obrazu od ich tła. Wynikowy obraz może zostać w prosty sposób wykorzystany w celu analizy i rozpozna- wania obiektów. Spełniony musi być tylko jedyny waru- nek: poprawne rozróżnienie istotnych elementów od tła. Biblioteka OpenCV umożliwia szybkie przeprowadzenie binaryzacji z wykorzystaniem tak zwanego progowania. Technika ta polega na analizowaniu kolejnych pikseli i po- #include #include #include int main() { CvCapture* obraz = cvCaptureFromCAM (CV_ CAP_ANY); if ( !obraz ) { fprintf( stderr, "Brak kamery\n" ); return -1; } cvNamedWindow( "okienko", CV_WINDOW_ AUTOSIZE ); while ( 1 ) { IplImage* klatka = cvQueryFrame( obraz ); if ( !klatka ) { fprintf( stderr, "Brak klatki\n" ); break; } IplImage* szary = cvCreateImage (cvSize(klatka->width, klatka->height), IPL_ DEPTH_8U, 1 ); IplImage* koncowy = cvCreateImage (cvSize(klatka->width, klatka->height) , 8, 1 ); cvCvtColor(klatka, szary, CV_BGR2GRAY); // filtr Gaussa cvSmooth(szary, szary, CV_GAUSSIAN, 9, 9); // binaryzacja obrazu (progowanie) cvThreshold( szary, koncowy, 100, 255, CV_THRESH_BINARY ); // zamiast binaryzacji w niektórych zastosowaniach lepiej skorzystać z algorytmu Canny'ego do wykrywania krawędzi //cvCanny( szary, koncowy, 75, 150, 3 ); //cvDilate( koncowy, koncowy, 0, 1 ); cvShowImage( "okienko", koncowy ); cvReleaseImage( &szary ); cvReleaseImage( &koncowy ); if ( ( cvWaitKey(10) & 255 ) == 27 ) break; } cvReleaseCapture( &obraz ); cvDestroyWindow( "okienko" ); return 0; } Listing 4. Przygotowanie obrazu do wykrywania obiektów: filtry i binaryzacja

15/ www.programistamag.pl / ANALIZA OBRAZU: ROZPOZNAWANIE OBIEKTÓW #include #include #include int main() { CvCapture* obraz = cvCaptureFromCAM (CV_ CAP_ANY); if ( !obraz ) { fprintf( stderr, "Brak kamery\n" ); return -1; } cvNamedWindow( "okienko", CV_WINDOW_ AUTOSIZE ); CvSize parzysty; while ( 1 ) { IplImage* klatka = cvQueryFrame( obraz ); if ( !klatka ) { fprintf( stderr, "Brak klatki\n" ); break; } // skalowanie obrazu parzysty = cvSize( klatka->width & -2, klatka->height & -2 ); IplImage* szary = cvCreateImage( parzysty, IPL_DEPTH_8U, 1 ); IplImage* maly = cvCreateImage( cvSize(parzysty.width/2, parzysty.height/2), IPL_DEPTH_8U, 1 ); cvSetImageROI( klatka, cvRect( 0, 0, parzysty.width, parzysty.height )); cvCvtColor(klatka, szary, CV_BGR2GRAY); cvPyrDown( szary, maly, 7 ); cvPyrUp( maly, szary, 7 ); // wykrywanie krawędzi algorytmem Canny IplImage* koncowy = cvCreateImage( parzysty, 8, 1 ); cvCanny( szary, koncowy, 75, 150, 3 ); cvDilate( koncowy, koncowy, 0, 1 ); // przygotowanie zmiennych przechowujących dane linii CvMemStorage* storage = cvCreateMemStorage(0); CvSeq* linie = 0; // transformacja Hougha linie = cvHoughLines2(koncowy, storage, CV_HOUGH_PROBABILISTIC, 1, CV_PI/180, 50, 50, 10 ); // wykreślanie linii na obrazie źródłowym for (int i = 0; i < linie->total; i++ ) { CvPoint* linia = (CvPoint*) cvGetSeqElem(linie,i); cvLine(klatka, linia[0], linia[1], CV_ RGB(255,0,0), 3, 8 ); } cvShowImage( "okienko", klatka ); // porządki cvReleaseImage( &maly ); cvReleaseImage( &szary ); cvReleaseImage( &koncowy ); if ( ( cvWaitKey(10) & 255 ) == 27 ) break; } cvReleaseCapture( &obraz ); cvDestroyWindow( "okienko" ); return 0; } równywania ich z założoną wartością (lub wartościami). Punkty o wartościach znajdujących się poza ustalonym przez użytkownika zakresem są oznaczane zerem bądź jedynką, natomiast pozostałe – drugą z wartości. Tym sa- mym wynikowy obraz nie zawiera żadnych barw innych niż czerń i biel – jest zatem zbinaryzowany. Kluczowe znaczenie dla procesu progowania ma wy- bór progu. Jego nieprawidłowe ustalenie spowoduje, że elementy obrazu staną się niemożliwe do wykrycia, lub ich krawędzie znajdą się w złym miejscu. Wybór odpo- wiedniego progu wymaga niekiedy wielu eksperymen- tów; w niektórych sytuacjach można pozostawić decyzję oprogramowaniu, poszukując obiektów na obrazie kilka- krotnie, dla różnych wartości granicznych. Daje to lepsze rezultaty, ale wymaga czasu, utrudniając analizę dostar- czanych na bieżąco klatek filmu z kamery. Progowania dokonujemy za pomocą funkcji cvThre- shold. Jeżeli chcemy uzyskać obraz binarny, to oprócz klatki/zdjęcia źródłowego i wynikowego, wartości progu Listing 5. Wykrywanie prostych metodą transformacji Hougha oraz maksymalnej, musimy jeszcze określić wartość pią- tego parametru jako CV_THRESH_BINARY. Progowanie nie musi bowiem koniecznie skutkować binaryzacją. Przy- kładowe polecenie wykorzystujące funkcję cvThreshold znaleźć można na listingu 4. Na zbinaryzowanym obrazie widoczne są nie tylko obiekty, ale także ich krawędzie. Nie jest to jedyny spo- sób, by odnaleźć na zdjęciach i klatkach wideo wyraźne granice, czyli linie wzdłuż których wartości sąsiadujących pikseli zmieniają się gwałtownie. Inne rozwiązanie pole- ga na użyciu algorytmów wykrywania krawędzi. OpenCV oferuje kilka spośród nich. Pełną listę można znaleźć w dokumentacji biblioteki. Jednym z najbardziej znanych algorytmów wykrywania krawędzi jest metoda Canny’ego. W przypadku OpenCV detekcja granic pomiędzy elementami i tłem sprowadza się do użycia funkcji cvCanny z pięcioma parametrami – obrazami wejściowym i wyjściowym, dwoma progami oraz rozmiarem maski dla operatora Sobela. Progi można

16 / 1 . 2012 . (1)  / PROGRAMOWANIE GRAFIKI #include #include #include int main() { CvCapture* obraz = cvCaptureFromCAM (CV_ CAP_ANY); if ( !obraz ) { fprintf( stderr, "Brak kamery\n" ); return -1; } cvNamedWindow( "okienko", CV_WINDOW_ AUTOSIZE ); CvSize parzysty; while ( 1 ) { IplImage* klatka = cvQueryFrame( obraz ); if ( !klatka ) { fprintf( stderr, "Brak klatki\n" ); break; } parzysty = cvSize( klatka->width & -2, klatka->height & -2 ); IplImage* szary = cvCreateImage( parzysty, IPL_DEPTH_8U, 1 ); IplImage* maly = cvCreateImage( cvSize(parzysty.width/2, parzysty.height/2), IPL_DEPTH_8U, 1 ); cvSetImageROI( klatka, cvRect( 0, 0, parzysty.width, parzysty.height )); cvCvtColor(klatka, szary, CV_BGR2GRAY); cvPyrDown( szary, maly, 7 ); cvPyrUp( maly, szary, 7 ); CvMemStorage* storage = cvCreateMemStorage(0); // zmiana w stosunku do kodu wykorzystującego funkcję cvHoughLines2 -- obraz jest przygotowany za pomocą filtra Gaussa cvSmooth(szary, szary, CV_GAUSSIAN, 9, 9); // wykrywanie okręgów CvSeq* circles = cvHoughCircles(szary, storage, CV_HOUGH_GRADIENT, 2, szary- >height/4, 200, 100); // wykreślanie wykrytych okręgów na obrazie źródłowym int i; for (i = 0; i < circles->total; i++) { float* p = (float*)cvGetSeqElem( circles, i ); cvCircle( klatka, cvPoint(cvRound(p [0]),cvRound(p[1])), cvRound(p[2]), CV_ RGB(255,0,0), 3, 8, 0 ); } cvShowImage( "okienko", klatka ); cvReleaseImage( &maly ); cvReleaseImage( &szary ); if ( ( cvWaitKey(10) & 255 ) == 27 ) break; } cvReleaseCapture( &obraz ); cvDestroyWindow( "okienko" ); return 0; } określać w sposób dowolny (w zakresie od 0 do 255 w przypadku obrazach w odcieniach szarości), pamiętając jednak, że główne krawędzie są najpewniej wykrywane, gdy rozstaw tych parametrów jest duży (listing 4). DETEKCJA PROSTYCH I OKRĘGÓW Binaryzacja i detekcja granic w obrębie obrazów prowadzi nas do głównego zagadnienia – czyli wykrywania oraz roz- poznawania obiektów. Także i w tym przypadku OpenCV oferuje spory pakiet możliwości. Jeżeli naszym zadaniem jest odnalezienie na obrazie prostych bądź okręgów, to powinniśmy skorzystać z kilku funkcji implementujących transformację Hougha. Dzięki nim cały proces wykrywa- nia sprowadza się do wydania jednego polecenia, dzięki któremu otrzymamy kompletną listę odnalezionych ele- mentów. Adresy konkretnych punktów na obrazie pozwo- lą nam na przetwarzanie tych informacji. Listing 6. Wykrywanie okręgów za pomocą transformacji Hougha Transformacja Hougha służy do wykrywania kształ- tów opisanych równaniem określonym przez użytkowni- ka. Początkowo wykorzystywano ją do detekcji prostych, krzywych i okręgów. Po opracowaniu uogólnionej trans- formacji Hougha metoda ta zaczęła się sprawdzać w wy- krywaniu dowolnych kształtów. OpenCV zawiera kilka funkcji opartych na algorytmach Hougha. Podstawowe narzędzie to cvHoughLines2. Funk- cja ta pozwala na wykrycie linii na odpowiednio przygoto- wanym obrazie, zwracając współrzędne znalezionych pro- stych. Na listingu 5 transformacja Hougha wykorzystana jest w celu detekcji prostych na klatkach wideo. Zostały one wcześniej przeskalowane i przetworzone algorytmem Canny’ego. Pierwsza z czynności może zostać zastąpiona filtrem bądź pominięta, w zależności od potrzeb. Efekty pracy metody Hougha nanoszone są na obraz źródłowy, wyświetlany na ekranie.

17/ www.programistamag.pl / ANALIZA OBRAZU: ROZPOZNAWANIE OBIEKTÓW W podobny sposób transformacja Hougha może zostać wykorzystana w celu wykrywania okręgów. Pozwala na to funkcja cvHoughCircles. Jej przykładowe zastosowanie można znaleźć na listingu 6. Użycie transformacji Hougha zostało tam poprzedzone filtrem Gaussa. WYKRYWANIE DOWOLNYCH OBIEK- TÓW Bibliotekę OpenCV możemy wykorzystać nie tylko w celu detekcji prostych i okręgów, ale także dowolnych kształ- tów geometrycznych. W tym celu należy zastosować funkcję cvFindContours, która odnajdzie wszystkie wi- doczne na obrazie kontury. Oczywiście konieczne jest od- powiednie przygotowanie zdjęcia lub klatki filmu. Obraz powinien być poprawnie zbinaryzowany (tak, by wido­czne były wszystkie potrzebne obiekty). OpenCV pozwala na wygodne manipulowanie na wykrytych konturach, co zo- stało zaprezentowane na listingu 7. Znalezione kontury są tam poddawane analizie mającej na celu detekcję prosto- kątów. Wykryte kształty są generalizowane, tak by uzy- skać wielokąty (funkcja cvApproxPoly). Następnie, o ile uzyskana figura ma dokładnie cztery wierzchołki, oblicza- ne są kąty pomiędzy sąsiednimi bokami. Pozwala to na zidentyfikowanie prostokątów. Figury, które spełniają wy- mienione kryteria, są wykreślane na obrazie źródłowym. W podobny sposób można tworzyć kod rozpoznający inne wielokąty. W przypadku bardziej skomplikowanych figur jest to jednak trudne. Dodatkowym problemem sta- #include #include #include // zmodyfikowana wersja demo "Square Detector" // oblicza cosinus kąta, umożliwiając odnalezienie kątów prostych double angle( CvPoint* pt1, CvPoint* pt2, CvPoint* pt0 ) { double dx1 = pt1->x - pt0->x; double dy1 = pt1->y - pt0->y; double dx2 = pt2->x - pt0->x; double dy2 = pt2->y - pt0->y; return (dx1*dx2 + dy1*dy2)/sqrt((dx1*dx1 + dy1*dy1)*(dx2*dx2 + dy2*dy2) + 1e-10); } int main() { CvCapture* obraz = cvCaptureFromCAM (CV_ CAP_ANY); if ( !obraz ) { fprintf( stderr, "Brak kamery\n" ); return -1; } CvSize parzysty; int rozmiary_klatki = (int) (cvGetCaptureProperty(obraz, CV_CAP_PROP_ FRAME_WIDTH) * cvGetCaptureProperty(obraz, CV_CAP_PROP_FRAME_HEIGHT) ); cvNamedWindow( "okienko", CV_WINDOW_ AUTOSIZE ); CvSeq* prostokaty; while ( 1 ) { IplImage* klatka = cvQueryFrame( obraz ); if ( !klatka ) { fprintf( stderr, "Brak klatki\n" ); break; } parzysty = cvSize( klatka->width & -2, klatka->height & -2 ); IplImage* szary = cvCreateImage( parzysty, IPL_DEPTH_8U, 1 ); IplImage* maly = cvCreateImage( cvSize(parzysty.width/2, parzysty.height/2), IPL_DEPTH_8U, 1 ); cvSetImageROI( klatka, cvRect( 0, 0, parzysty.width, parzysty.height )); cvCvtColor(klatka, szary, CV_BGR2GRAY); cvPyrDown( szary, maly, 7 ); cvPyrUp( maly, szary, 7 ); IplImage* koncowy = cvCreateImage( parzysty, 8, 1 ); // deklaracje zmiennych przechowujących kontury oraz prostokąty CvSeq* kontury; CvPoint* rect; CvMemStorage* storage = 0; // przygotowanie obrazu; binaryzacja, wykrywanie krawędzi int l, N = 11; for( l = 0; l < N; l++ ) { if( l == 0 ) { // wykrywanie krawędzi za pomocą algorytmu Canny cvCanny( szary, koncowy, 0, 50, 5 ); cvDilate( koncowy, koncowy, 0, 1 ); } else { // binaryzacja metodą progowania cvThreshold( szary, koncowy, (l+1)*255/N, 255, CV_THRESH_BINARY ); } Listing 7. Wykorzystanie OpenCV do detekcji wielokątów cd. na kolejnej stronie...

18 / 1 . 2012 . (1)  / PROGRAMOWANIE GRAFIKI // wykrywanie konturów na przygotowanym wcześniej obrazie storage = cvCreateMemStorage(0); cvFindContours(koncowy, storage, &kontury, sizeof(CvContour), CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE ); // dodatkowe deklaracje double powierzchnia_max = 0.0; CvPoint pt[4]; prostokaty = cvCreateSeq( 0, sizeof(CvSeq), sizeof(CvPoint), storage ); CvSeq* rezultat; double s, t; int i; while( kontury ) { rezultat = cvApproxPoly( kontury, sizeof(CvContour), storage, CV_POLY_APPROX_ DP, cvContourPerimeter(kontury)*0.02, 0 ); // poszukiwanie konturów o czterech wierzchołkach, o powierzchni większej od minimalnej dozwolonej (aby usunąć artefakty) // oraz mniejszej od całej powierzchni klatki if( rezultat->total == 4 && fabs(cvContourArea(rezultat,CV_WHOLE_ SEQ)) > 1000 && cvCheckContourConvexity(rezultat) && (fabs(cvContourArea(rezultat,CV_WHOLE_SEQ)) < ( (rozmiary_klatki / 10) * 9.5 )) ) { s = 0; for( i = 0; i < 5; i++ ) { if( i >= 2 ) { t = fabs(angle( (CvPoint*)cvGetSeqElem( rezultat, i ), (CvPoint*)cvGetSeqElem( rezultat, i-2 ), (CvPoint*)cvGetSeqElem( rezultat, i-1 ))); s = s > t ? s : t; } } // cosinus kątów bliski zeru, a więc kontur jest prostokątem if( s < 0.2 ) { for( i = 0; i < 4; i++ ) cvSeqPush( prostokaty, (CvPoint*)cvGetSeqElem( rezultat, i )); // wykryty prostokąt jest największym z widocznych na ekranie if ( fabs(cvContourArea(rezultat,CV_ WHOLE_SEQ)) > powierzchnia_max ) { powierzchnia_max = fabs(cvContourArea(rezultat,CV_WHOLE_SEQ)); CvSeqReader reader; cvStartReadSeq( rezultat, &reader, 0 ); for( int ii = 0; ii < rezultat- >total; ii += 4 ) { rect = pt; memcpy( pt, reader.ptr, prostokaty->elem_size ); CV_NEXT_SEQ_ELEM( rezultat- >elem_size, reader ); memcpy( pt + 1, reader.ptr, rezultat->elem_size ); CV_NEXT_SEQ_ELEM( rezultat- >elem_size, reader ); memcpy( pt + 2, reader.ptr, rezultat->elem_size ); CV_NEXT_SEQ_ELEM( rezultat- >elem_size, reader ); memcpy( pt + 3, reader.ptr, rezultat->elem_size ); CV_NEXT_SEQ_ELEM( rezultat- >elem_size, reader ); } } } } // przejście do kolejnego konturu kontury = kontury->h_next; } // wykreślenie prostokąta na obrazie źródłowym if ( klatka && rect ) { int counting = 4; cvPolyLine( klatka, &rect, &counting, 1, 1, CV_RGB(0,255,0), 3, 8 ); } } cvShowImage( "okienko", klatka ); cvReleaseImage( &maly ); cvReleaseImage( &szary ); cvReleaseImage( &koncowy ); if ( ( cvWaitKey(10) & 255 ) == 27 ) break; } cvReleaseCapture( &obraz ); cvDestroyWindow( "okienko" ); return 0; } cd... Listing 7. Wykorzystanie OpenCV do detekcji wielokątów

19/ www.programistamag.pl / ANALIZA OBRAZU: ROZPOZNAWANIE OBIEKTÓW je się dokładna parametryzacja znalezionych konturów. Lepszym rozwiązaniem jest wykorzystanie klasyfikatora kaskadowego Haara. Umożliwi on rozpoznawanie dowol- nych obiektów, bez konieczności pisania dodatkowego kodu C++. W OpenCV wymaga to skorzystania z funk- cji cvHaarDetectObjects. Wcześniej należy jeszcze zała- dować plik klasyfikatora w formacie XML. Służy do tego funkcja cvLoad. Większa ilość kodu nie jest potrzebna. Przykładowy, kompletny program obejrzeć można w do- kumentacji. Napisanie kodu korzystającego z algorytmu Haara to jednak nie wszystko. Przed rozpoczęciem rozpoznawania obiektów należy bowiem przygotować własny klasyfika- tor. W tym celu trzeba dostarczyć zestaw próbek zawie- rających szukany obiekt, a także takich, w których go nie ma. Obrazy te zostaną użyte w celu trenowania klasyfi- katora. Narzędzia do tego przeznaczone rozpowszechnia- ne są razem z biblioteką OpenCV. Jeżeli wystarczą nam gotowe klasyfikatory, pozwalające na wykrywanie sylwe­ tki ludzkiej oraz twarzy jej części, to możemy skorzystać z istniejących plików XML, umieszczonych w podkatalogu data\haarcascades. Tworzenie własnego klasyfikatora jest czynnością trój- etapową. Pierwszy krok polega na skorzystaniu z progra- mu opencv_createsamples. Na podstawie jednego lub kil- ku zdjęć obiektu utworzy on szereg obrazków, w których obiekt będzie umieszczony na innym tle. Po przygotowa- niu próbek można przystąpić do trenowania klasyfikatora. Służy do tego program opencv_haartraining. Otrzymany klasyfikator warto przetestować za pomocą polecenia opencv_performance. Od tego momentu można już roz- poznawać interesujące nas obiekty. Nie trzeba nic zmie- niać w kodzie – poza ścieżką dostępu do wygenerowane- go przez nas pliku XML. PODSUMOWANIE Rozpoznawanie obiektów na obrazach to obszerne zagad- nienie, do niedawna trudne do zastosowania w prakty- ce. Pojawienie się aplikacji oraz usług wykorzystujących opisane techniki sprawiło, że znajomość komputerowej analizy obrazu stała się przydatna coraz większemu gro- nu programistów. Zmiany te idą w parze z pojawieniem się wszechstronnych i zaawansowanych bibliotek umożli- wiających wykrywanie obiektów na obrazach i w sekwen- cjach wideo. Dzięki temu przygotowanie własnej aplikacji korzystającej z technik analizy obrazu jest stosunkowo proste i nie zajmuje wiele czasu. Paweł Wolniewicz pawelw@innodevel.net Autor od pięciu lat wykorzystuje metody analizy obrazów biologicznych, przede wszystkim z użyciem biblioteki OpenCV. reklama

20 / 1 . 2012 . (1)  / JĘZYKI PROGRAMOWANIA Rafał Kocisz Kilka miesięcy temu miałem okazję zmierzyć się z za- daniem polegającym na stworzeniu biblioteki służącej do budowania graficznych interfejsów użytkownika zintegrowa- nej z silnikiem do tworzenia gier na nowoczesne platformy mobilne oraz przenośne konsole do gier. Zmagając się z tym fascynującym i - jak się w praktyce okazało - dość złożonym zagadnieniem, napotkałem sze- reg ciekawych problemów do rozwiązania. Jednym z tych problemów było stworzenie uniwersalnego, a jednocześnie wygodnego w obsłudze mechanizmu wywołań zwrotnych (ang. callbacks) w języku C++. Mechanizm taki w końcu udało mi się zbudować. Zapro- gramowałem go, korzystając z konstrukcji języka C++ opisa- nych w standardzie ANSI/IOS z 1998 (głównie ze względu na brak wsparcia dla konstrukcji oraz biblioteki standardowej najnowszego standardu języka w starszych kompilatorach). Później jednak zdecydowałem się w ramach eksperymentu stworzyć podobny mechanizm, korzystając z możliwości, ja- kie oferuje C++Ox.Wyniki tego eksperymentu oraz związane z nim przemyślenia stały się impulsem, który skłonił mnie do napisania niniejszego artykułu, do lektury którego serdecznie zapraszam! W szystko zaczęło się od zadania. W moim przy- padku chodziło o stworzenie biblioteki kontrolek wielokrotnego użytku, za pomocą której będzie można szybko budować oraz utrzymywać przenośne in- terfejsy użytkownika w grach uruchamianych na szeregu urządzeń mobilnych. Przez "przenośność" rozumiem tutaj zestaw cech polegających na tym, iż wspomniane inter- fejsy potrafią się dopasowywać do różnych rozdzielczości wyświetlaczy, do liczby wyświetlaczy (niektóre mobilne konsole posiadają więcej niż jeden ekran), a także od- powiednio obsługiwać różne zestawy kontrolerów dostę­ pnych na danej platformie (np. ekran dotykowy, d-pad, ekran dotykowy + d-pad itd.). Ogólne założenia techniczne dotyczące tej biblioteki były takie, że klasa reprezentująca stany gry (GameState) "trzyma" w sobie drzewiastą hierarchię kontrolek interfej- su użytkownika (ControlTree) i przekazuje do niej zdarze- nia wygenerowane w związku z interakcją użytkownika (np. wciśnięcie klawisza, dotknięcie ekranu czy wychyle- nie urządzenia). Komunikacja w tę stronę odbywa się bez problemu, np. poprzez wywoływanie wirtualnych funkcji z bazowej klasy reprezentującej abstrakcyjną kontrolkę (Control), obsługujących różne typy zdarzeń. Na Listingu 1 pokazane są uproszczone definicje wymienionych wyżej klas, obrazujące podstawowe relacje między nimi. Analizując Listing 1, warto zwrócić uwagę na to, w jaki sposób klasa BrowseMainMenuState deleguje zdarze- nia OnKeyDown() oraz OnKeyUp() do drzewa kontrolek. Uważni Czytelnicy zauważą zapewne szybko dość istotną lukę w przedstawionej architekturze. Chodzi oczywiście o komunikację zwrotną. Np. załóżmy, że obiekt klasy BrowseMainMenuState przechwyci komunikat OnKey- C++11 w praktyce: sygnały i sloty Sygnały i sloty to ciekawy wariant mechanizmu wywołań zwrotnych stanowiący ele- gancką alternatywę dla wzorca Obserwator. Czytając niniejszy artykuł zrozumiesz, jak działa ten mechanizm, przekonasz się, jak można zaoszczędzić sobie pracy, stosu- jąc go, i przekonasz się, jak można go łatwo zaimplementować, korzystając z nowo- czesnych możliwości języka C++. Down (jak to się dzieje, a bardziej ogólnie - w jaki sposób można efektywnie i wygodnie zarządzać stanami gry to oddzielny, bardzo ciekawy temat - być może wrócimy do niego w jednym z przyszłych odcinków tego cyklu). Ko- munikat ten zostanie natychmiast przekazany do drzewa kontrolek, a za jego pośrednictwem do poszczególnych kontrolek, aż trafi na tę właściwą kontrolkę, która powin- na w odpowiedzi wygenerować konkretną akcję. Aczkol- wiek tutaj pojawia się pytanie: co wtedy zrobić? A kon- kretnie, w jaki sposób klasa BrowseMainMenuState ma być powiadomiona o tym fakcie? Rozwiązaniem, które można by zastosować w tej sytu- acji, jest wzorzec Obserwator (ang. Observer). W prakty- ce mogłoby wyglądać to następująco. Do przedstawionej hierarchii klas należałoby dodać interfejs ButtonObserver: class ButtonObserver { public: virtual void OnButtonActivated() = 0; }; Ogólna idea stosowania wzorca Obserwator polega na tym, że obiekt obserwowany (w naszym przypadku: Button) rejestruje obserwatorów (tj. obiekty klas dzie- dziczących z interfejsu ButtonObserver) i w odpowied- nim momencie wywołuje na nich operacje notyfikujące o zaistnieniu określonego zdarzenia (w naszym przy- padku: OnButtonActivated). Na Listingu 3 (patrz obok) przedstawione są definicje klas BrowseMainMenuState oraz Button zmodyfikowane zgodnie z przedstawionym wyżej opisem.

21/ www.programistamag.pl / C++11 W PRAKTYCE: SYGNAŁY I SLOTY Listing 1. Uproszczone definicje klas wchodzących w skład biblioteki kontrolek Na Listingu 4 (patrz kolejna strona) przedstawio- na jest przykładowa implementacja konstruktora klasy BrowseMainMenuState, która przedstawia, w jaki spo- sób mogłaby wyglądać rejestracja obserwatora w obie­ kcie klasy Button. W ten sposób udało się nam zaimplementować me- chanizm komunikacji zwrotnej dla kontrolki reprezen- tującej przycisk. Zakładamy, że w sytuacji gdy obiekt stanu przechwyci zdarzenie będące wynikiem interakcji użytkownika z aplikacją, przekaże go do drzewa kontro- lek, aż trafi ono w końcu do kontrolki reprezentującej przycisk, gdzie zostanie obsłużone, i jeśli zajdzie taka potrzeba, to na obiekcie stanu wywołana będzie akcja zwrotna OnButtonActivated(), w której można umieścić implementację logiki reagującej na to zdarzenie. Myślisz Czytelniku, że na tym kończą się problemy? Też tak myślałem, kiedy pracując nad prototypem mojej biblioteki, zaimplementowałem opisany wyżej mecha- nizm. Niestety, rozwiązanie okazało się być dalekie od ideału... // Reprezentuje abstrakcyjną kontrolkę // interfejsu użytkownika. // class Control { public: virtual bool OnKeyDown(int keyCode); virtual bool OnKeyUp(int keyCode); }; // Reprezentuje drzewiastą hierarchię // obiektów typu Control. // class ControlTree { public: bool OnKeyDown(int keyCode); bool OnKeyUp(int keyCode); }; // Reprezentuje bazową klasę dla wszystkiego // rodzaju kontrolek // przycisków. // class Button : public Control { public: virtual bool OnKeyDown(int keyCode); virtual bool OnKeyUp(int keyCode); }; // Reprezentuje abstrakcyjny stan gry. // class GameState { public: virtual bool OnKeyDown(int keyCode); virtual bool OnKeyUp(int keyCode); }; // Reprezentuje przykładowy stan gry. // class BrowseMainMenuState : public GameState { public: virtual bool OnKeyDown(int keyCode) { return m_ControlTree ->OnKeyDown(keyCode); } virtual bool OnKeyUp(int keyCode) { return m_ControlTree ->OnKeyUp(keyCode); } private: ControlTreePtr m_ControlTree; }; class BrowseMainMenuState : public GameState , public ButtonObserver { public : virtual bool OnKeyDown(int keyCode) { return m_ControlTree->OnKeyDown(keyCode); } virtual bool OnKeyUp(int keyCode) { return m_ControlTree->OnKeyUp(keyCode); } private: virtual void OnButtonActivated(); private: ControlTreePtr m_ControlTree ; }; class Button : public Control { public: virtual bool OnKeyDown(int keyCode); virtual bool OnKeyUp(int keyCode); public : void RegisterObserver(ButtonObserver *observer); }; Listing 3. Zmodyfikowane definicje klas BrowseMainMenuState oraz Button

22 / 1 . 2012 . (1)  / JĘZYKI PROGRAMOWANIA zania jest mechanizm wiążący konkretne metody klasy reprezentującej stan ze zdarzeniami generowanymi przez kontrolki UI (zwróć uwagę na punkt 2 w komentarzach umieszczonych w ciele konstruktora klasy BrowseMain- MenuState przedstawionego na Listingu 5). Jednakże tu- taj pojawia się kluczowe pytanie: jak takie powiązanie zaimplementować? SYGNAŁY I SLOTY (SIGNALS AND SLOTS) Odpowiedzią jest mechanizm sygnałów i slotów, interesu- jąca metoda rozsyłania zdarzeń w aplikacjach, wykorzy- stywana przede wszystkim w interfejsach użytkownika. Mechanizm ten został w dużej mierze spopularyzowany przez Qt (http://qt.nokia.com/), jedną z najpopularniej- szych, niezależnych od platformy bibliotek wspierających tworzenie graficznych interfejsów użytkownika. O sygna- łach i slotach mówi się, że to wariant wzorca Obserwator, w którym potrzeba tworzenia powtarzalnego kodu spro- wadzona jest do minimum. Jak działa ten mechanizm, najłatwiej przekonać się w praktyce. Na Listingu 6 przed- stawiłem zestaw klas z naszego przykładu, zmodyfikowa- nych tak, aby korzystały z mechanizmu sygnałów i slo- tów. Czy dostrzegasz już elegancję mechanizmu sygnałów i slotów? Rozważmy najważniejsze elementy na wspomnia- nym listingu. Na początku, w klasie Button deklarujemy, że obiekty tejże klasy mogą emitować sygnał (zdarzenie) nazwane "Activated". Dzieje się to za pomocą umieszcze- nia makra SIGNAL w definicji tej klasy. W przykładowej implementacji metody Button::OnKeyDown pokazane jest, w jaki sposób można emitować sygnał. Robi się to za pomocą wyrażenia: EMIT(NazwaSygnału, ()); w naszym przypadku będzie to: EMIT(Activated, ());. Deklaracja klasy reprezentującej stan (BrowseMainMenuState) pozo- staje bez zmian, zaś w jej konstruktorze łączymy sygnał Activated emitowany przez przycisk playButton z metodą OnPlayButtonActivated() wołaną dla instancji obiektu tej- że klasy. I na tym koniec. Od tego momentu za każdym razem, kiedy przycisk "PLAY" zostanie aktywowany, wywo- łana będzie metoda BrowseMainMenuState::OnPlayButto- nActivated() na obiekcie playButton. Prawda, że proste? O ile z punktu widzenia użytkownika korzystanie z me- chanizmu sygnałów i slotów daje wrażenie prostoty i ele- BrowseMainMenuState::BrowseMainMenuState() { // ... // Wyłuskaj obiekt reprezentujący // przycik z drzewa kontrolek. // Button* button = /* ... */; button->RegisterObserver(this); // } Listing 4. Rejestracja obserwatora w obiekcie klasy ButtonPROBLEMY Pierwszy, podstawowy problem, który napotkałem po zastosowaniu wzorca Obserwator w mojej bibliotece, to kwestia obsługi wielu kontrolek jednocześnie. Wyobraź sobie proszę, że obiekt reprezentujący stan przegląda- nia głównego menu gry (BrowseMainMenuState) musi komunikować się z zestawem kilku przycisków, np. GRA, OPCJE, POMOC. Jest to dość powszechny scenariusz. W tej sytuacji w konstruktorze klasy BrowseMainMenu- State należałoby rejestrować obiekt stanu (this) jako ob- serwatora w kilku obiektach reprezentujących poszcze- gólne przyciski. Jednakże podążając tą drogą, trafiamy na pewien koncepcyjny zgrzyt: w jaki sposób wewnątrz metody OnButtonActivated() dowiedzieć się, który przy- cisk został faktyczne aktywowany? Prostym rozwiąza- niem tego problemu może być następująca modyfikacja sygnatury funkcji OnButtonActivated(): virtual void OnButtonActivated(const Button* activatedButton)=0; Przy takim podejściu zakładamy, że aktywowany przycisk wywołujący metodę OnButtonActivated() przekaże do niej wskaźnik na siebie samego. To poniekąd rozwiązuje sprawę, jednakże nadal musimy w jakiś sposób wyłuskać z przekazanego obiektu informację o tym, z kim mamy do czynienia. Tutaj pojawia się problem nadawania kon- trolkom unikalnych identyfikatorów, co dodatkowo kom- plikuje sprawę. Na tym jednak problemy się nie kończą. Otóż kiedy przyjdzie nam oprogramować bardziej skomplikowaną logikę obsługi zdarzeń generowanych przez kontrolki, okazuje się, że kończy się to na napisaniu sporych roz- miarów konstrukcji switch, w której poszczególne sek- cje case obsługują akcje przypisane do poszczególnych przycisków. Oczywiście, fragmenty kodu odpowiadające poszczególnym sekcjom można odpowiednio pozamykać w oddzielnych funkcjach, jednakże nie zmienia to faktu, że utrzymywanie tego rodzaju kodu w przypadku bardziej złożonych projektów staje się sporym narzutem. ROZWIĄZANIE Moje pierwsze eksperymenty z prototypową implementa- cją biblioteki (czytaj: próba jej zastosowania do skonstru- owania części interfejsu użytkownika w prawdziwej grze) skończyły się fiaskiem. Co prawda, wszystko działało, jednakże kod obsługujący logikę interfejsu użytkownika po stronie gry był nieakceptowalny (zbyt rozdmuchany, bardzo uciążliwy w utrzymywaniu). W tej sytuacji za- cząłem się zastanawiać, jak - patrząc z punktu widzenia użytkownika tej biblioteki - powinien wyglądać ten kod. Listing 5 przedstawia wizję, która pojawiła się w mojej głowie. W komentarzach zawartych na listingu znajdują się wyjaśnienia opisujące działanie tego rozwiązania. Jeśli przeczytałeś uważnie wspomniane komentarze, to zgodzisz się zapewne ze mną, iż takie rozwiązanie byłoby o wiele bardziej przyjazne dla użytkownika w porównaniu do stosowania wzorca Obserwator. Sercem całego rozwią-

23/ www.programistamag.pl / C++11 W PRAKTYCE: SYGNAŁY I SLOTY Listing 5. Obsługa logiki interfejsu użytkownika gancji, o tyle z punktu widzenia jego implementacji w ję- zyku C++ (zgodnego ze standardem C++98), sprawa nie wygląda już tak różowo. Dla przykładu, Qt implementuje ten mechanizm za pomocą specjalnego rozszerzenia ję- zyka obsługiwanego przez tzw. kompilator meta-obiektów (ang. Meta-Object Compiler). W praktyce oznacza to, że aby skorzystać z mechanizmu sygnałów i slotów dostępnych w bibliotece Qt, Twój kod źródłowy musi być przed właściwą kompilacją przetworzony odpowiednim narzędziem. Istnie- ją również implementacje tego mechanizmu obywające się bez dodatkowych faz kompilacji, chociażby Boost.Signals (http://www.boost.org/doc/html/signals.html), jednakże ich implementacja jest bardzo skomplikowana. Ja na potrze- by swojej biblioteki GUI opracowałem dość minimalistyczną implementację tego mechanizmu. Pomimo swojej prostoty implementacja ta zajmuje kilkaset linii kodu i w dość pokręt- ny sposób łączy zaawansowane techniki metaprogramowa- nia opartego na szablonach oraz wymyślne makra. // Zauważ, że BrowseMainMenuState nie // musi implementować żadnych // dodatkowych interfejsów jak w przypadku // korzystania ze wzorca Obserwator. // class BrowseMainMenuState : public GameState { public : // Funkcje obsługujące zdarzenia aktywacji // poszczególnych klawiszy; nie muszą być // wirtualne. // void OnPlayButtonActivated(); void OnOptionsButtonActivated(); void OnHelpButtonActivated(); void OnExitButtonActivated(); private: ControlTreePtr m_ControlTree ; }; BrowseMainMenuState::BrowseMainMenuState() { Button * playButton = NULL; // 1. Wyłuskaj z drzewa kontrolek obiekt // reprezentujący przycisk "PLAY". // 2. Powiąż metodę BrowseMainMenuState:: // OnPlayButtonActivated() z aktywacją // przycisku // reprezentowanego przez obiekt // playButton. // 3. Wykonaj analogiczne akcje dla // pozostałych przycisków. } void BrowseMainMenuState ::OnPlayButtonActivated() { // Ta funkcja będzie wywołana za każdym // razem kiedy ktoś aktywuje przycisk // "PLAY". // Umieść tu logikę obsługującą to // zdarzenie. } // Analogiczne implementacje pozostałych // funkcji obsługujących zdarzenia class Button : public Control { SIGNAL(Activated, ()); public: virtual bool OnKeyDown(int keyCode); }; void Button::OnKeyDown(int keyCode) { // Sprawdź czy należy aktywować przycisk. // ... // Jeśli warunki aktywacji zostały // spełnione, emituj // sygnał "Activated". EMIT(Activated, ()); } class BrowseMainMenuState : public GameState { public: void OnPlayButtonActivated(); // private: ControlTreePtr m_ControlTree; }; BrowseMainMenuState::BrowseMainMenuState() { Button* playButton = NULL; // Wyłuskaj z drzewa kontrolek obiekt // reprezentujący przycisk // "PLAY". // ... CONNECT(playButton, Activated, this, BrowseMainMenuState ::OnPlayButton Activated); // W analogiczny sposób połącz sygnał // "Activated" // emitowany przez pozostałe przyciski z // odpowiednimi // slotami. } Listing 6. Przykład wykorzystania mechanizmu sygnałów i slotów

24 / 1 . 2012 . (1)  / JĘZYKI PROGRAMOWANIA Główny problem, z którym przychodzi się borykać progra- mistom implementującymy mechanizm sygnałów i slotów, to brak wsparcia dla tzw. adapterów obiektów funkcyjnych w języku C++, czyli konstrukcji, która pozwoliłaby opako- wać dowolne wywołanie funkcji i traktować je jako pełno- prawny obiekt. Kiedy skończyłem implementować mechanizm sygnałów i slotów w C++98, zacząłem zastanawiać się, jak można by to samo zadanie zrealizować za pomocą C++11. Okazało się, że wykorzystując możliwości najnowszej wersji języka C++, implementacja opisanego wyżej mechanizmu staje się zdecydowanie łatwiejsza, niemalże banalna - przynajmniej w porównaniu z jej odpowiednikiem opartym na C++98. W dalszej części artykułu pokażę, jak można to zadanie zre- alizować. INTERMEZZO Zanim przejdziemy do finalnej części artykułu, chciał- bym poruszyć kilka kwestii dotyczących C++11 w ujęciu praktycznym. Jak powszechnie wiadomo, standard ISO/ IEC 14882:2011 (znany powszechnie jako C++11) został opublikowany we wrześniu 2011. Jak to jednak w życiu bywa - ze wsparciem dla tego standardu ze strony kom- pilatorów jest różnie (sytuacja tutaj zmienia się de facto z dnia na dzień, więc podaruję sobie próby opisania, jakie fragmenty specyfikacji wspierają poszczególne kompila- tory). Nie da się jednak ukryć, że do skompilowania ko- lejnych przykładów przedstawionych w niniejszym tekście będziesz potrzebował kompilatora (w końcu chcemy zaj- mować się C++11 w ujęciu praktycznym!). Ja osobiście opracowując źródła dla niniejszego artykułu, korzystałem z Microsoft Visual Studio C++ 2010 Express Edition. Jest to darmowe narzędzie i z marszu kompiluje sporą część kodu zgodnego ze standardem C++11. Używając go, nie powinieneś mieć żadnych problemów, z kompilacją kodów źródłowych przedstawionych na listingach umieszczonych w niniejszym tekście. Podejrzewam, że nie będziesz miał z tym również problemów korzystając z najświeższej wer- sji GCC bądź z kompilatora LLVM dołączonego do najnow- szej wersji środowiska Xcode. Ogólnie dobrym sposobem przekonania się, czy kompilator wspiera C++11, jest pró- ba zbudowania przykładowego programu, np. takiego jak przedstawiono na Listingu 7. Jeśli Twój kompilator bez problemu "przełknie" ten kod, to możesz śmiało kontynu- ować lekturę artykułu. SYGNAŁY I SLOTY W C++11 Jak zaimplementować mechanizm sygnałów i slotów w C++11? Implementację tego mechanizmu (docelowo powinna ona znajdować się w pliku Signal.hpp) przedsta- wiłem na Listingu 8 (nast. strona). Rozważmy krok po kroku, co się tutaj dzieje. Aby wy- konać zadanie, musimy zmierzyć się z implementacją trzech podstawowych elementów funkcjonalności. Pierw- szy z nich to definicja sygnału (makro SIGNAL), drugi to emisja sygnału (makro EMIT), trzeci - to mechanizm łączenia sygnałów i slotów (rodzina makr CONNECT). Pierwszy element realizujemy za pomocą wstrzykiwa- nia fragmentu definicji klasy. Definicja makra SIGNAL rozpoczyna się od słowa kluczowego określającego za- kres widoczności (public), po którym umieszczona jest definicja inline funkcji Add#signal#Listener(). Widzimy, że pre-procesor składa odpowiednio nazwę tej funk- cji, wklejając w jej środek nazwę sygnału. Dla naszego przykładowego sygnału nazwanego "Activated" nazwa funkcji miałaby postać AddActivatedListener. Funkcja ta jako argument przyjmuje obiekt typu std::function. std::function to szablon klasy reprezentu- jącej tzw. polimorficzny adapter obiektów funkcyjnych. Szablon ten jest częścią biblioteki standardowej C++11 (aby z niego skorzystać, należy dołączyć nagłówek); pozwala on tworzyć obiekty podo­bne semantycznie i składniowo do wskaźników do funkcji, z tą różnicą, że mogą reprezentować wszystko, co może być w języku C++ wywołane (funkcje wolne, wskaźniki do funkcji, wskaźniki do metod, obiekty funcyjne itd.), zakładając oczywiście zgodność arguemntów. Dla przy- kładu definicja adaptera, zdolnego do reprezentowania dowolnego obiektu funkcyjnego, który zwraca wartość int, a przyjmuje dwa argumenty: int oraz referencję do std::string, wyglądałaby następująco: std::function fun(...); W miejscu trzech kropek można przekazać adres dowol- nego obiektu funcyjnego, który ma identyczną sygnaturę jak nasz adapter. Wracając do naszej implementacji, ana- lizując implementację funkcji Add##signal##Listener, możemy się przekonać, że omawiane adaptery rzeczywi- ście są pełnoprawnymi obiektami. W naszym przypadku po prostu zapamiętujemy adapter w kontenerze std::vec- tor. Kontener ten jest wstrzykiwany jako prywatna skła- dowa obiektu, za pośrednictwem makra SIGNAL. Jego nazwa (m_##signal##Listeners) wiąże się z tym, że #include #include #include #include using namespace std; int main() { vector words; words.push_back("Hello"); words.push_back(", "); words.push_back("world"); words.push_back("!"); for_each(words.begin(), words.end(), [](string& word) { cout << word; }); return 0; } Listing 7. "Witaj, Świecie!" w stylu C++11

25/ www.programistamag.pl / C++11 W PRAKTYCE: SYGNAŁY I SLOTY przechowuje on obiekty nasłuchujące (można też powie- dzieć: funkcje zwrotne) dla konkretnego typu sygnału. Gdybyśmy chcieli użyć makra SIGNAL w naszym przykła- dzie z przyciskami, to moglibyśmy w definicji klasy But- ton umieścić definicję: SIGNAL(Activated, ()); W praktyce to by oznaczało, że klasa ta będzie emito- wać sygnał Activated, bez przekazywania dodatkowych argumentów. Warto w tym miejscu zwrócić uwagę, że typ wartości zwracanej dla wszystkich funkcji zwrotnych uży- wanych w mechanizmie sygnałów i slotów jest domyślnie określony jako void. Przejdzmy teraz do definicji makra EMIT. Makro to po- zwala wyemitować sygnał. Widząc jak zaimplemetowa- ne zostało makro SIGNAL, łatwo się domyśleć, że emisja sygnału będzie polegać na wywołaniu funkcji zwrotnych przechowywanych w kontenerze przyporządkowanym dla konkrentego sygnału. W ten właśnie sposób zdefionio- wane jest makro EMIT. Korzystjąc z pętli for, przegląda- my kontener i aktywujmey kolejno zapamiętane w nim obiekty funkcyjne. Warto w tym miejscu zwrócić uwagę na pewien szczegół, a mianowicie na konstrukcję "for each in" użytą w implementacji makra EMIT pokazanej na Listingu 7. Konstrukcja ta jest rozwiązaniem specy- ficznym dla środowiska Microsoft Visual Studio, które nie obsługuje póki co standardowej konstrukcji for pozwala- jącej iterować po elementach określonego zakresu. Jeśli korzystasz z innego kompilatora niż Visual Studio C++, to zamień tę kłoptliwą linię na: for(auto func : m_##signal##Listeners) \ Korzystanie z makra EMIT jest bardzo podobne do ko- rzystania z makra SIGNAL, z tą różnicą, że zamiast prze- kazywania nazw typów podajemy konkretne argumenty. Gdybyśmy chcieli użyć makra EMIT w naszym przykładzie z przyciskami, to moglibyśmy gdzieś w implementacji wy- branej metody Button umieścić wyrażenie: EMIT(Activated, ()); Gdybyśmy z jakichś przyczyn zażyczyli sobie, aby w trak- cie emisji sygnału przekazywać jakiś argument (np. war- tość całokowitą reprezentującą identyfikator kontrolki), to sygnał należałby zdefiniować następująco: SIGNAL(Activated, (int)); Emisja sygnał z kolei miałaby postać: EMIT(Activated, this->Id()); Zakładam tutaj, że Id() to metoda zdefioniwana gdzieś w bazowej klasie Control, zwracająca jej identyfikator. Ostatni brakujący element układanki to mechanizm łączenia sygnałów i slotów. W przedstawionej imple- mentacji służy do tego rodzina makr CONNECT. Dlacze- go rodzina? A no dlatego, że zakładamy obsługę funkcji zwrotnych o różnych liczbach argumentów. Stąd wła- śnie nazwy makr CONNECT1, CONNECT2, CONNECT3, itd. Każde z tych makr obsługuje funkcje zwrotne o licz- bie argumentów odpowiadającej wartości dołączonej do nazwy makra. Same definicje poszczególnych makr są proste: na początku za pomocą asercji sprawdzamy, czy emiter nie jest przypadkiem pustym obiektem (za- uważ, że w porównaniu używamy nowego słowa kluczo- wego nullptr, jako zamiennika dla starego, poczciwego NULL'a). Pozostaje jeszcze stworzenie odpowiedniego adaptera obiektu funkcyjnego. Na szczęście bibliote- ka standardowa C++11 oferuje niezwykle pożyteczny szablon std::bind, który w tym przypadku realizuje do- kładnie to, czego oczekujemy: wiąże konkretny obiekt (obserwatora) ze slotem (tj. z metodą tego obiektu) i z argumentami. Ponieważ na etapie łączenia nie znamy jeszcze argumentów, trzeba użyć obiektów zastępczych, które są zdefiniowane w przestrzeni nazw std::placehol- ders. Do obiektów tych odnosimy się, pisząc: std::place- holders::_1, std::placeholders::_2, itd. Na Listingu 7 przedstawiłem implementację makr CON- NECT1, CONNECT2 oraz CONNECT3. Kolejne makra z tej rodziny można definiować poprzez analogię, a najlepiej jest napisać sobie prosty program w ulubionym języku skryptowym (np. Perl, Python czy Ruby) który wygeneru- je nam określoną liczbę definicji tego makra. Zadanie to pozostawiam do realizacji jako ćwiczenie dla dociekliwych Czytelników. SYGNAŁY I SLOTY W DZIAŁANIU I tak oto mamy działający mechanizm sygnałów i slo- tów zaimplementowany w języku C++11 (zadziwiająco szybko poszło, nieprawdaż?). Dobrze by było przetesto- wać go w praktyce. Umieszczenie przykładu związanego z rzeczywistą biblioteką kontrolek interfejsu użytkownika (nawet bardzo prostą) pod każdym względem przekracza ramy tego artykułu. Możemy się jednak pokusić o prosty przykład wykorzystujący zwykłą konsolę tekstową. Przy- kład taki przedstawiony jest na Listingu 8. Mamy tutaj definicje dwóch prostych klas emitują- cych sygnały: przycisk (Button) oraz suwaka (Gauge). Przycik emituje sygnał stanowiący powiadomienie o jego aktywacji. Suwak z kolei powiadamia o zmianie swojego ustawienia, przekazując przy tym liczbę zmiennoprze- cinkową z zakresu 0.f-1.f określajacą wartość, na któ- rą został ustawiony. Obydwa obiekty umieszczone są w klasie GameState, która przechwytuje sygnały emi- towane przez wspomniane obiekty. Połączenie nastę- puje w konstruktorze klasy GameState. W funkcji main tworzony jest obiekt stanu, a następnie wywoływane są odpowiednio funkcje Activate() i Set() na obydwu kon- trolkach, w wyniku czego następuje emisja sygnałów. Na wyjściu programu powinny pojawić się komunikaty o przechwyceniu sygnałów, wypisane z funkcji zwrot- nych GameState::OnPlayButtonActivated() oraz Game- State::OnVolumeGaugeSet().