Java + OpenCV + repaint()

Java + OpenCV + repaint()
UC
  • Rejestracja:prawie 11 lat
  • Ostatnio:prawie 11 lat
  • Postów:3
0

Witajcie, jest to mój pierwszy post na forum i dopiero raczkuję w świecie programowania. Próbuję stworzyć program z niezbyt skomplikowanym GUI (kilka zakładek, przycisków, comboBoxów i duży JPanel) który umożliwiałby detekcję twarzy z obrazu (to się udało!) oraz ze strumienia video (a to niezupełnie!). Problem polega na tym, że po pobraniu obrazu z kamery w formacie Mat (OpenCV) i przeniesieniu tego do formatu BufferedImage próbuję ten obraz w pętli wyświetlić na JPanelu, ale uzyskuję oczywiście tylko ostatnią klatkę z całej pętli.

W przypadku, gdy pisałem próbny kod, wszystko działało dobrze, dopiero po przeniesieniu do większego programu, składającego się z kilku klas wewnętrznych, wielu metod i większego GUI, pojawił się problem. Oto fragment kodu, który wydaje się nie działać poprawnie:

Kopiuj
class MojStartZ2Listener implements ActionListener {

		public void actionPerformed(ActionEvent a) {
			switch (wyborOpcjiZ2) {
			case 0:
				odtworzVideo();
				break;
			case 1:
				break;
			case 2:
				break;
			default:
				break;
			}
		}

		public void odtworzVideo() {
			System.loadLibrary(Core.NATIVE_LIBRARY_NAME);

			Mat obrazKamery = new Mat();
			VideoCapture kamera = new VideoCapture(0);
			int i = 0;
			if (kamera.isOpened()) {
				try {
					Thread.sleep(800);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				while (i < 10) {
                               //Tutaj argumentem było "true", ale dodałem zmienną, aby sprawdzić co się wydarzy po skończeniu pętli,
                               //i wtedy odkryłem, że ostatni obraz jest wyświetlany na JPanelu
					i++;
					kamera.read(obrazKamery);
					if (!obrazKamery.empty()) {

						MatToBufferedImage zmiana = new MatToBufferedImage();
						obrazVideoZ2 = zmiana.getBuffed(obrazKamery);

						obrazPanelRysunekZ2.repaint();
						try {
							Thread.sleep(10);
						} catch (Exception ex) {

						}
						String filename = "D:/Eclipse/Projekty2/Obrazy/faceDetectionKamera.png";
						Highgui.imwrite(filename, obrazKamery);
					}
				}
			}
		}
	}// Koniec klasy

Obrazy są zapisywane w ścieżce filename w czasie rzeczywistym, więc miałem "podgląd" swojej kamery, obserwując zawartość folderu, więc wiem, że na pewno problem leży w kontakcie klasy ActionListenera (który jest klasą wewnętrzną) z metodą TworzGUI() klasy zewnętrznej, która rysuje całe GUI, w tym panel. Czytając różne wątki zdążyłem wydedukować, że problem najprawdopodobniej leży w wątkach i w tym, że metoda repaint() przy wywoływaniu w pętli ma tendencję do działania "jak jedno" wywołanie, które uwidacznia się dopiero po zakończeniu działania całej pętli. Niestety, nie wiem jak sobie z tym problemem poradzić.

Nie do końca rozumiem obsługę wątków (ich wykorzystanie w praktyce, poza metodami sleep :)) i nie wiem jak połączyć jedno z drugim, aby ostatecznie, po kliknięciu przycisku na JPanelu uwidocznił mi się obraz z kamery, dlatego prosiłbym by nie pisać zbyt fachowych terminów. Dziękuję z góry za każdą odpowiedź, i pozdrawiam.

edytowany 1x, ostatnio: UncleChris
UC
  • Rejestracja:prawie 11 lat
  • Ostatnio:prawie 11 lat
  • Postów:3
0

Witam ponownie, spędziłem wiele godzin czytając o wątkach, i spróbowałem zastosować takie rozwiązanie, że całą metodę odtworzVideo() przeniosłem do nowej klasy wewnętrznej, która implementowała interfejs Runnable. W metodzie run umieściłem kod, który wcześniej był w metodzie odpowiedzialnej za wyświetlanie obrazu z kamery. Następnie w switchu utworzyłem nowy wątek, którego argumentem był konstruktor tej nowej klasy wewnętrznej + metoda start(). No i działa!

Kod "uruchamiający", czyli odpowiedź na kliknięcie przycisku:

Kopiuj
	class MojStartZ2Listener implements ActionListener {

		public void actionPerformed(ActionEvent a) {
			switch (wyborOpcjiZ2) {
			case 0:
				new Thread(new OdtworzVideoWNowymWatku()).start();
				break;
			case 1:
				break;
			case 2:
				break;
			default:
				break;
			}
		}
	}// Koniec klasy

Nowo dodana klasa wewnętrzna:

Kopiuj
 public class OdtworzVideoWNowymWatku implements Runnable {
		public void run() {
			
				System.loadLibrary(Core.NATIVE_LIBRARY_NAME);

				Mat obrazKamery = new Mat();
				VideoCapture kamera = new VideoCapture(0);
				int i = 0;
				if (kamera.isOpened()) {
					try {
						Thread.sleep(800);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					while (i < 1000) {
						i++;
						kamera.read(obrazKamery);
						if (!obrazKamery.empty()) {

							MatToBufferedImage zmiana = new MatToBufferedImage();
							obrazVideoZ2 = zmiana.getBuffed(obrazKamery);

							obrazPanelRysunekZ2.repaint();
							try {
								Thread.sleep(10);
							} catch (Exception ex) {

							}
							String filename = "D:/Eclipse/Projekty2/Obrazy/faceDetectionKamera.png";
							Highgui.imwrite(filename, obrazKamery);
						}
					}
				}	
		}
	}

Pozdrawiam!

Olamagato
  • Rejestracja:ponad 16 lat
  • Ostatnio:14 dni
  • Lokalizacja:Polska, Warszawa
  • Postów:1058
3

Twój pierwszy i najpoważniejszy błąd, to nieprawdopodobnie czasochłonna obsługa zdarzenia. Powinna ona zająć maksymalnie ułamek milisekundy, a zajmuje czas, który może być liczony w sekundach. Obsługa zdarzeń jest jednowątkowa. Oznacza to, że dopóki nie zakończysz swojej obsługi zdarzenia, żadne inne zdarzenie, ani nawet odrysowanie gui nie zostanie obsłużone.
Są tylko dwie zasady dotyczące obsługi zdarzeń:

  1. Żadnych czasochłonnych operacji!
  2. Jeżeli korzystasz ze sleep, operacji plikowej lub tworzenia obiektów (new/fabryka), to jeszcze raz wszystko przeczytaj, skasuj ten kod i wróć do punktu 1.

U Ciebie obsługa zdarzenia kończy się dopiero po tym jak zmarnujesz 0,8s na pierwsze sleep oraz nieokreślony czas na utworzenie obiektu VideoCapture, a następnie w pętli kolejny raz marnujesz 10 razy po 10 ms oraz nieokreślony czas na zapis obrazu do pliku (prawdopodobnie). W efekcie tracisz co najmniej sekundę na obsługę jednego zdarzenia. W tym czasie całe GUI obsługiwane przez Javę jest kompletnie zamrożone i czeka na zakończenie Twojej procedury obsługi zdarzenia.

W jednym przebiegu pętli obsługi zdarzeń wszystkie polecenia repaint dotyczące tego samego obszaru obrazu są scalane w jedno wywołanie. Obsługa pojedynczego zdarzenia jest jednym z elementów jednego przebiegu takiej pętli. Samej pętli nie widzisz ponieważ jest ona schowana w klasach Swinga i w jej trakcie przebiegane są wszystkie procedury:

  • obsługi zdarzeń, wszystkich obiektów, które mogą je wygenerować
  • aktualizacji obiektów (w tym żądania repaint)
  • odmalowania zaktualizowanych obiektów
    Krótko mówiąc z punktu widzenia obsługi GUI Twoja procedura jest niczym innym jak nieświadomym wandalizmem. :)

Teraz jak to naprawić.
Twoja obsługa przycisku ma za zadanie jedynie włączyć przełącznik pozwalający Twojemu programowi na samodzielne odrysowywanie kolejnych klatek pobieranych z jakiegoś źródła. I na tym ma się zakończyć. Ponieważ jednak potrzebne jest przygotowanie źródła, otwarcie plików i ewentualna obsługa błędów (obsługa pliku zawsze może wygenerować błąd i/o) - to musisz to zrobić najlepiej przed momentem w którym będziesz mógł otrzymać pierwsze żądanie obsługi zdarzenia. I tu właśnie wchodzi obsługa wątków.
Pierwszym wątkiem z jakim masz do czynienia jest wywołanie metody main w przypadku aplikacji lub init w przypadku apletu. Najlepiej więc z tego miejsca przygotować wszystko do przeprowadzania płynnej animacji, czytania z plików, strumieni itp.
Drugim wątkiem z jakim masz do czynienia jest wywołanie obsługi zdarzenia. Zazwyczaj wszystkie procedury obsługi wywoływane są w jednym wątku w jakiejś kolejności - dlatego mówi się, że obsługa GUI jest jednowątkowa.
Wbrew pozorom wątek odpalający metodę main nie musi się kończyć, choć w przypadku apletu init powinna zostać zakończona bo nie ma żadnej gwarancji, ze init, start, stop i destroy muszą być koniecznie osobnymi wątkami.
Wątek kręcący pętlą obsługi GUI uruchamia kiedy zostanie wykonana pierwsza operacja, która będzie wymagać jej działania - np. setVisible(true) lub addXXXListener.

Nie jesteś jednak ograniczony tymi zdefiniowanymi wątkami. Wszędzie gdzie potrzeba możesz utworzyć oraz następnie uruchomić nowy wątek. Tworzenie i uruchamianie nie musi być w jednym miejscu (nawet lepiej, żeby nie było). Jeżeli Twoja procedura obsługi potrzebuje więcej czasu na operacje, to zawsze może uruchomić inny wątek i tam mogą być te operacje kontynuowane. Aktualnie nowe wątki nie powinno odpalać się przez tworzenie obiektu Thread (to jest bardzo niski poziom wymagający drobiazgowej obsługi), lecz przez wysokopoziomowe klasy jak SwingWorker czy obiekty zadań (Runnable, Callable) wykonywane przez różne typy egzekutorów oraz timery.
W Twoim wypadku procedura obsługi naciśnięcia przycisku powinna uruchomić wątek timera, który w wywołaniu zegarowym co najmniej 25 razy na sekundę będzie odczytywał kolejny obraz z kamery, przygotowywał go w formacie nadającym się do odmalowania w wybranym oknie/komponencie, a następnie wyrzucał żądanie repaint dla tego okna/komponentu.
To żądanie wyzwoli wywołanie metody paint/paintComponent (zależnie na jak wysokim poziomie chcesz odmalowywać kontrolkę), w której będziesz mógł po prostu odpalić metodę draw na już przygotowanym obiekcie obrazka. Ważne, że czas wykonania wywołania zegarowego też jest ograniczony do maksymalnie 1/25 sekundy ponieważ jeżeli wykona się dłużej, to rozpocznie się już kolejne wywołanie zegarowe, które zacznie wczytywać kolejną klatkę itd. Wtedy jeżeli deficyt czasu będzie narastał doprowadzi to do awarii danych i katastrofy.

Zamiast timera można też użyć osobnego wątku, który będzie wykonywał pętlę aktywnego renderingu (tak jak robią to profesjonalne gry). Dzięki temu można uniknąć katastrofy w przypadku chwilowych opóźnień (tzw. laga), który może wyniknąć z przyczyn nad którymi nie masz żadnej kontroli (np. defragmentacji pamięci, kalibracji kamery, opóźnień sygnału itp.). Aktywny rendering pozwala bowiem na bieżący pomiar czasu i pominięcie niektórych najbardziej czasochłonnych elementów w sytuacji deficytu czasu, którego nie ma jak nadgonić. Jednak na Twoim poziomie jest to zbyt trudne, więc masz tylko informację, że istnieje lepsza alternatywa dla Timera.

Jeżeli więc ograniczysz się tylko do Timera, to musisz kontrolować czas wykonania każdego wywołania zegarowego i ograniczyć go do okresu czasu wynikającego z podziału sekundy na ilość klatek, którą chcesz/musisz uzyskać. Najlepiej jednak podziel go jeszcze na 2 lub 4 bo sam timer i reszta systemu też ma swój narzut czasowy, a aplikację możesz przecież wykonywać na systemie jednoprocesorowym. Musisz też utrzymywać flagę informującą czy jesteś w trakcie wykonywania wywołania zegarowego tak aby każde następne wywołanie zegarowe mogło ją sprawdzić i w przypadku gdy poprzednie wywołanie się nie zakończyło natychmiast zakończyć obsługę bieżącego wywołania (lub w czasie testów generować wyjątek oraz przerywać działanie aplikacji). To pozwoli uniknąć katastrofy - gdyby taka miała nastąpić.

Krótko podsumowując:

  1. main/init - przygotowujesz pliki, kamerę, obiekty potrzebne do płynnego odczytu takie jak timer itp., na komponencie, który ma wyświetlać obraz odpalasz setIgnoreRepaint(true), żeby nie było niekontrolowanych odrysowań gdy Twoje obrazy nie są jeszcze gotowe.
  2. Metoda actionPerformed - ustawiasz flagę aktywności animacji i uruchamiasz timer do generowania wywołań zegarowych
  3. Metoda wywołania zegarowego sprawdza czy aktualizacja jest w trakcie (wtedy wychodzi), ustawia flagę aktualizacji, odczytuje obraz z kamery, przygotowuje go do wyświetlenia i wyłącza flagę aktualizacji przed wyjściem.
  4. Metoda paint (dla JPanel) lub paintComponent (dla innych) odrysowuje przygotowany obraz i natychmiast kończy się.
  5. O sleep powinieneś zapomnieć, że w ogóle istnieje. De facto sleep powoduje utworzenie nowego wątku, który nic nie robi, tylko czeka, a po jego zakończeniu robi join z wątkiem, który sleep wywołał. Dlatego można ją przerwać przed czasem i dlatego to przerwanie jest obligatoryjnie do złapania.

[aktualizacja]
W międzyczasie sobie poradziłeś, ale to nadal jest tylko działająca prowizorka, w której ręcznie sterujesz. Działa to głównie dzięki zbiegowi okoliczności i dobrej jakości kodu bibliotek, które wywołujesz. :)


Jeżeli ktoś komuś coś, ewentualnie nikt nikomu nic, to właściwie po co...?
edytowany 1x, ostatnio: Olamagato
UC
  • Rejestracja:prawie 11 lat
  • Ostatnio:prawie 11 lat
  • Postów:3
0

Bałem się, że nikt się nie zainteresuje moim problemem, albo zostanie przerzucony do Newbie i zginie w katuszach, a tu proszę, taka konstruktywna i treściwa odpowiedź! Bardzo dziękuję za poświęcony mi czas, teraz spróbuję to ponownie dokładnie przeanalizować i zastosować w praktyce :).

Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)