Politechnika Wrocławska Urządzenia peryferyjne sprawozdanie z laboratorium Ćwiczenie 12: Kamera USB 02.01.12 r. Michał Kaczara (181132) Pn / TN, 14:15 17:00 Prowadzący: dr inż. Jarosław Mierzwa 1. Zadania do wykonania Zadania do wykonania na zajęciach laboratoryjnych obejmowały następujące czynności: napisanie aplikacji obsługującej kamerę USB, umożliwiającej wybór kamery, wyświetlanie obrazu, zapisywanie zdjęcia, kopiowanie klatki do schowka, nagrywanie obrazu, zmianę ustawień i rozdzielczości kamery, publikację zdjęcia na stronie WWW oraz detekcję ruchu. 2. Wstęp teoretyczny Kamera USB (popularnie zwana kamerą internetową) to zewnętrzne urządzenie rejestrujące obraz wideo w czasie rzeczywistym, podłączane do komputera przez port USB. Kamery internetowe znajdują liczne zastosowania w codziennym życiu. Służą przede wszystkim do komunikacji oraz nagrywania krótkich plików wideo, czy też robienia zdjęć. Ze względu na swoją prostą budowę i niewielką cenę urządzenia tego typu transmitują obraz nienajlepszej jakości. Najczęściej spotykane kamery posiadają matryce CMOS o niewielkich rozmiarach i rozdzielczości VGA (640x460) zapisujące strumień z częstotliwością 30 klatek na sekundę(fps ang. frames per second). Taka liczba klatek jest wystarczająca do uzyskania płynnego obrazu(minimum to 24 klatki na sekundę). Istnieje wiele metod obsługi kamery podłączonej do komputera poprzez port USB. W systemie Windows można skorzystać z biblioteki avicap32.dll, twain32.dll lub z API do WIA 1.0 i 2.0 oraz DirectX(Direct Show). Na zajęciach skorzystałem z biblioteki avicap32.dll, ponieważ jej obsługa jest stosunkowo prosta w implementacji i łatwa w zrozumieniu. 3. Realizacja ćwiczenia W ramach realizacji ćwiczenia, napisany został program w języku C++ z wykorzystaniem bibliotek MFC. Za komunikację z użytkownikiem odpowiedzialne jest okno dialogowe, umożliwiające: wybór kamery, którą ma obsługiwać program, włączenie / wyłączenie wybranej kamery poprzez kliknięcie na przycisk Włącz kamerę lub Wyłącz kamerę, zapisanie zdjęcia do pliku o nazwie podanej w polu tekstowym, poprzez kliknięcie na przycisk Zapisz zdjęcie, skopiowanie zdjęcia do schowka poprzez kliknięcie na przycisk Kopiuj obraz do schowka, rozpoczęcie / zakończenie nagrywania poprzez kliknięcie na przycisk Rozpocznij nagrywanie lub Zakończ nagrywanie, zmianę ustawień i rozdzielczości kamery poprzez działania w oknach dialogowych zależnych od sterownika kamery, wywoływanych kolejno przyciskami Ustawienia i Zmień rozdzielczość, otworzenie strony WWW z obrazem z kamery poprzez kliknięcie na przycisk Strona WWW, obserwację stanu detekcji ruchu algorytm po wykryciu ruchu sygnalizuje ten fakt napisem RUCH pojawiającym się po prawej stronie okna aplikacji, wyjście z programu poprzez kliknięcie na przycisk Wyjście. 1
Rysunek 1. Interfejs okna głównego programu KameraUSB Z uwagi na ograniczoną podczas laboratorium ilość czasu, nie zdecydowałem się na pisanie oddzielnej klasy do obsługi kamery USB. Odpowiednie instrukcje zawarłem w funkcjach klasy okna dialogowego, przypisanych do przycisków na nim umieszczonych. Metody zapewniające podstawową obsługę kamery są funkcjami globalnymi. Ponieważ bibliotekę avicap32.dll obsługuje się poprzez wysyłanie odpowiednich komunikatów z określonymi parametrami(funkcja SendMessage()), konieczne było zdefiniowanie listy wiadomości potrzebnych do zaimplementowania aplikacji. Każdy z komunikatów jest odpowiedzialny za określoną funkcjonalność sterownika kamery. Definicje listy wiadomości wraz z komentarzami zawiera listing 1. Listing 1. Definicja listy komunikatów niezbędnych do obsługi kamery USB poprzez bibliotekę avicap32.dll //Podstawa komunikatow static const int WM_CAP = WM_USER; //Polaczenie ze sterownikiem static const int WM_CAP_DRIVER_CONNECT = WM_CAP + 10; //Rozlaczenie ze sterownikiem static const int WM_CAP_DRIVER_DISCONNECT = WM_CAP + 11; //Ustawienie funkcji Callback dla bledow static const int WM_CAP_SET_CALLBACK_ERROR = WM_CAP + 2; //Ustawienie funkcji Callback dla klatek static const int WM_CAP_SET_CALLBACK_FRAME = WM_CAP + 5; //Skopiowanie obrazu do schowka static const int WM_CAP_EDIT_COPY = 0x41e; //Wlaczenie / wylaczenie trybu podgladu static const int WM_CAP_SET_PREVIEW = WM_CAP + 50; //Ustawienie ilosci klatek static const int WM_CAP_SET_PREVIEWRATE = WM_CAP + 52; //Ustawienie mozliwosci skalowania obrazu static const int WM_CAP_SET_SCALE = WM_CAP + 53; //Ustawienie pliku tymczasowego dla nagrywania static const int WM_CAP_FILE_SET_CAPTURE_FILE = WM_CAP + 20; //Rozpoczecie nagrywania do pliku static const int WM_CAP_SEQUENCE = WM_CAP + 62; //Zakonczenie nagrywania 2
static const int WM_CAP_STOP = WM_CAP + 68; //Zapisanie pliku static const int WM_CAP_FILE_SAVEAS = WM_CAP + 23; //Odebranie i wyswietlenie ramki obrazu static const int WM_CAP_GRAB_FRAME = WM_CAP + 60; //Ustawienie rozdzielczosci static const int WM_CAP_SET_VIDEOFORMAT = WM_CAP + 45; //Zapis bitmapy static const int WM_CAP_FILE_SAVEDIB = WM_CAP + 25; //Okno zmiany zrodla i ustawien static const int WM_CAP_DLG_VIDEOSOURCE = WM_CAP + 42; //Okno zmiany rozdzielczosci static const int WM_CAP_DLG_VIDEOFORMAT = WM_CAP + 41; Funkcjami, które umożliwiają poprawną inicjalizację sterownika kamery są metody: initsterownik() i kamerainit(). Ta pierwsza przyjmuje jako parametr uchwyt okna, dla którego ma zostać wykonana inicjalizacja sterownika, druga zaś pięć parametrów strukturę z informacjami o kamerach, odpowiednio wypełnioną przez funkcję kamerawykryj() wykrywającą kamery podłączone do komputera, id kamery, wysokość obrazu, szerokość obrazu i uchwyt. Wymienione funkcje zastosowane zostały w metodzie OnInitDialog() klasy okna aplikacji. Jest to standardowa funkcja wykonywana podczas tworzenia okna. Metoda najpierw wywołuje funkcję OnInitDialog() rodzica klasy, następnie przypisuje do okna ikony, po czym następuje wykrycie kamer zainstalowanych w systemie, zapisanie ich parametrów oraz dodanie do listy wyboru urządzenia. W ramach tej funkcji odpowiednio modyfikowany jest także interfejs programu deaktywowany jest m.in. przycisk rozpoczynania nagrywania czy zapisywania zdjęcia, ustawiane są domyślne wartości pól tekstowych. Całą funkcję prezentuje listing 2. Listing 2. Metoda OnInitDialog() klasy CKameraUSBDlg BOOL CKameraUSBDlg::OnInitDialog() //MFC CDialog::OnInitDialog(); SetIcon(m_hIcon, TRUE); SetIcon(m_hIcon, FALSE); //Zapisanie wskaznika obiekt klasy okno = this; //Inicjalizacja DLL i sterownika initsterownik(m_hwnd); kamery = new kamera_info[10]; liczba_kamer = 0; //Wykrycie kamer kamerawykryj(kamery, liczba_kamer); //jesli brak kamer if(!liczba_kamer) return kamerablad("nie znaleziono zadnej kamery."); //Pobranie informacji o każdej z kamer i dodanie jej do listy wyboru CComboBox* cb = (CComboBox *)(GetDlgItem(IDC_LISTA)); for (int i = 0; i < liczba_kamer; ++i) wchar_t tmpnazwa[80]; mbstowcs(tmpnazwa, kamery[i].kam_nazwa, 80); cb->addstring(tmpnazwa); 3
//Ustawienie domyslnego wyboru kamery cb->setcursel (0); GetDlgItem(IDC_FILE_KLATKA)->SetWindowTextW(_T("fotka.bmp")); GetDlgItem(IDC_FILE_FILM)->SetWindowTextW(_T("nagranie.avi")); GetDlgItem(IDC_BTN_ZAPISZ_KLATKE)->EnableWindow(FALSE); GetDlgItem(IDC_BTN_ZAPISZ_FILM)->EnableWindow(FALSE); GetDlgItem(IDC_BTN_KOPIUJ)->EnableWindow(FALSE); return TRUE; Po poprawnej inicjalizacji okna dialogowego korzystający z programu ma możliwość wyboru kamery z listy rozwijanej i jej włączenia poprzez kliknięcie na przycisk Włącz kamerę. Metoda wywoływana po kliknięciu na ten przycisk sprawdza, czy istnieje już uchwyt kamery. Jeżeli nie, to dokonuje jego stworzenia z użyciem funkcji kamerainit(), a następnie wysyła do sterownika komunikat VM_CAP_DRIVER_CONNECT. Po poprawnym połączeniu do sterownika ustawiane są kolejno: możliwość skalowania obrazu, ilość klatek na sekundę oraz tryb podglądu. Rejestrowana jest także funkcja zwrotna dla odbierania klatek - _kameraklatkagrab()- zdefiniowana na potrzeby realizacji detekcji ruchu. Następnie modyfikowany jest interfejs oraz włączany jest timer odpowiedzialny za zapisywanie zdjęcia co 2 sekundy(na potrzeby publikacji na stronie WWW). Jeżeli uchwyt kamery już istnieje, to metoda działa jako wyłącznik. Do sterownika wysyłany jest komunikat VM_CAP_DRIVER_DISCONNECT, po czym niszczony jest uchwyt, modyfikowany jest interfejs i wyłączany timer. Listing 3. Metoda wykonywana po kliknięciu na przycisk Włącz / Wyłącz kamerę void CKameraUSBDlg::OnBnClickedBtnOn() //Jesli brak uchwytu do kamery if(hkamera == NULL) //Inicjalizacja kamery hkamera = kamerainit(kamery[0], 0, kamera_dl, kamera_szer, GetDlgItem(IDC_KAM)->m_hWnd); //Jesli polaczenie udane if (::SendMessage(hKamera, WM_CAP_DRIVER_CONNECT, (WPARAM)kamery[obecna_kamera].index, 0L) ) //Ustawienie parametrow ::SendMessage(hKamera, WM_CAP_SET_SCALE, true, 0); ::SendMessage(hKamera, WM_CAP_SET_PREVIEWRATE, 0x42, 0); ::SendMessage(hKamera, WM_CAP_SET_PREVIEW, -1, 0); //Rejestracja funkcji typu Callback dla klatek ::SendMessage(hKamera, WM_CAP_SET_CALLBACK_FRAME, 0, (LPARAM)_kameraKlatkaGrab); GetDlgItem(IDC_BTN_ON)->SetWindowTextW(_T("Wylacz kamere")); GetDlgItem(IDC_BTN_ZAPISZ_KLATKE)->EnableWindow(TRUE); GetDlgItem(IDC_BTN_ZAPISZ_FILM)->EnableWindow(TRUE); GetDlgItem(IDC_BTN_KOPIUJ)->EnableWindow(TRUE); //Start timera sekund StartTimer(); //Jesli uchwyt juz istnieje else 4
//Wylaczeie kamery ::SendMessage(hKamera, WM_CAP_DRIVER_DISCONNECT, kamery[obecna_kamera].index, 0); //Zniszczenie uchwytu ::DestroyWindow(hKamera); hkamera = NULL; GetDlgItem(IDC_BTN_ON)->SetWindowTextW(_T("Wlacz kamere")); GetDlgItem(IDC_BTN_ZAPISZ_KLATKE)->EnableWindow(FALSE); GetDlgItem(IDC_BTN_ZAPISZ_FILM)->EnableWindow(FALSE); GetDlgItem(IDC_BTN_KOPIUJ)->EnableWindow(FALSE); //Stop timera sekund StopTimer(); Analogicznie do metody przypisanej przyciskowi włączania / wyłączania kamery, działa metoda przypisana do przycisku Rozpocznij nagrywanie. Sprawdza ona, czy nagrywanie jest w toku i jeśli tak, to otwiera plik *.avi w trybie zapisu, kończy nagrywanie(funkcja kameranagrywanieoff(), następuje zapis do pliku) i zamyka plik. Jeśli nie wywoływana jest funkcja kameranagrywanieon() z parametrem będącym nazwą pliku tymczasowego( tmp.avi ). W ramach wykonywania funkcji modyfikowany jest także interfejs aplikacji. Listing 4. Metoda wykonywana po kliknięciu na przycisk Rozpocznij / Zakończ nagrywanie void CKameraUSBDlg::OnBnClickedBtnZapiszFilm() //Pobranie danych z interfejsu - przycisk + nazwa pliku CButton* btn = (CButton *)GetDlgItem(IDC_BTN_ZAPISZ_FILM); CEdit* edit = (CEdit *)GetDlgItem(IDC_FILE_FILM); CString nazwapliku; edit->getwindowtextw(nazwapliku); CStringA nazwapliku1(nazwapliku); //Jesli nagrywanie trwa if(nagrywanie) //Otworzenie pliku do zapisu FILE* fp = fopen(nazwapliku1, "wb"); if (!fp) MessageBoxA (NULL, "Nie mozna otworzyc pliku do zapisu", "", 0); //Wylaczenie nagrywania i zamkniecie pliku kameranagrywajoff(nazwapliku1); fclose(fp); //Usuniecie pliku tymczasowego remove("tmp.avi"); btn->setwindowtextw(_t("rozpocznij nagrywanie")); //Jesli nie else //Wlaczenie nagrywania do pliku tymczasowego kameranagrywajon("tmp.avi"); btn->setwindowtextw (_T("Zakoncz nagrywanie")); //Zmiana stanu nagrywanie =!nagrywanie; 5
Metody związane z obsługą przypisane do pozostałych przycisków są proste, a ich działanie polega na przesłaniu do sterownika odpowiedniego komunikatu(listing 1.). Z tego względu pominę ich opis w sprawozdaniu. Metoda przypisana do przycisku Strona WWW także jest prosta, a jej działanie polega na wywołaniu funkcji ShellExecute() z odpowiednimi parametrami. Gdy do funkcji tej przekazane zostaną: drugi parametr = open i trzeci = adres strony WWW, otwarte zostanie okno domyślnej przeglądarki internetowej z załadowanym wskazanym adresem. Detekcję ruchu zrealizowałem w ramach funkcji zarejestrowanej jako zwrotna dla odbioru klatek jest ona wykonywana przy odbiorze z kamery każdej klatki obrazu, co pozwala na porównywanie danych odebranych ze sterownika i na tej podstawie zdecydowanie, czy wystąpił ruch. Aby opisana koncepcja mogła zostać zrealizowana, konieczne było zdefiniowanie struktury HDRVideo_t, opisującej każdą klatkę obrazu. Z punktu widzenia algorytmu jedynym potrzebnym polem było pole data, zawierające tablicę bajtów obrazu. Oprócz tego pola, zdefiniowana struktura zawiera jeszcze inne, np. pole size, określające rozmiar klatki, czy pole time, zawierające czas powstania. Funkcja kameraklatkadetekcja() realizująca algorytm detekcji ruchu działa wg prostych zasad. Jeśli poprzednia klatka jest pusta, to obecna klatka uznawana jest za poprzednią. Jeśli nie, to następuje porównanie obecnej klatki i poprzedniej klatki na podstawie liczby bajtów, dla których różnica wartości jest większa niż 50. Jeżeli takich bajtów jest więcej niż 10000, to program uznaje, że wystąpił ruch i w oknie dialogowym pokazywany jest napis RUCH. Jeżeli nie, napis RUCH jest ukrywany. Na końcu funkcji obecna klatka jest uznawana za poprzednią(następuje przekopiowanie danych spod adresu struktury obecnej klatki pod adres struktury poprzedniej klatki). Szczegóły prezentuje Listing 5. Listing 5. Struktura HDRVideo_t oraz Metoda kameraklatkadetekcja() realizująca algorytm detekcji ruchu //Struktura klatki obrazu typedef struct char* data; int size; int used; int time; int* user_data; int flags; int* reseved; HDRVideo_t; //Funkcja detekcji ruchu void kameraklatkadetekcja(hwnd h, HDRVideo_t* dane) //Jesli brak poprzedniej klatki if (!poprzednia_klatka) //Stworzenie i zapis danych poprzednia_klatka = new HDRVideo_t; poprzednia_klatka->size = dane->size; poprzednia_klatka->data = new char[dane->size]; //Obecna klatka obecna_klatka = dane; //Liczymy zmiane bajtow danych klatki //Jesli roznica bajtow < 50, liczymy bajt jako zmieniony int zmienione = 0; for (int i = 0; i < dane->size; i++) if (abs(poprzednia_klatka->data[i] - obecna_klatka->data[i]) > 50) zmienione++; 6
//Jesli zmian jest wiecej niz 10000 if (zmienione > 10000) //Ruch okno->getdlgitem(idc_ruch)->showwindow(sw_show); else //Brak ruchu okno->getdlgitem(idc_ruch)->showwindow(sw_hide); //Poprzednia klatka to obecna klatka memcpy(poprzednia_klatka->data, obecna_klatka->data, dane->size); Zaprezentowany algorytm detekcji ruchu działa dobrze i szybko tylko dla małych rozdzielczości. Przyczyną jest fakt, że aby stwierdzić, czy wystąpił ruch, trzeba przejść przez całą tablicę bajtów klatki. Zaimplementowane rozwiązanie przestaje być skuteczne już dla rozdzielczości VGA, chyba, że zostanie zmniejszona dokładność sprawdzania(np. co drugi, trzeci, czwarty bajt). Jest to więc słaby algorytm zrealizowany jedynie w celach dydaktycznych. 4. Wnioski Zaimplementowanie przedstawionego w sprawozdaniu programu obsługującego kamerę podłączoną do komputera poprzez interfejs USB pozwoliło mi na zapoznanie się z obsługą urządzeń audio/video z wykorzystaniem API biblioteki avicap32.dll. Program jest prosty, a jego stworzenie nie zajęło dużo czasu, obsługa tego typu urządzeń z poziomu języka C++ nie jest więc trudnym zadaniem. Po głębszym przejrzeniu dokumentacji byłem w stanie zaimplementować prosty algorytm detekcji ruchu, który jednak nie działał zbyt dobrze (nawet dla rozdzielczości VGA). Podczas realizacji zadania napotkałem też jeden problem gdy zgodnie z dokumentacją ustawiłem parametr fyield odpowiedzialny za realizację nagrywania w osobnym wątku, nagrywanie można było przerwać klikając gdziekolwiek w obszarze okna dialogowego. Dlatego też ostatecznie zrezygnowałem z tego rozwiązania i nagrywanie jest realizowane w wątku okna dialogowego. 7