Prosta rozszerzona rzeczywistość z OpenCV, Three.js i WebSockets

Opublikowany: 2022-03-10
Krótkie podsumowanie ↬ W tym samouczku użyjemy OpenCV w Pythonie do wykrywania obiektów w kształcie okręgów w strumieniu z kamery internetowej i zastąpimy je 3D Earth w Three.js w oknie przeglądarki, jednocześnie używając WebSockets do połączenia tego wszystkiego.

Rzeczywistość rozszerzona jest ogólnie uważana za bardzo trudną do stworzenia. Jednak możliwe jest tworzenie imponujących wizualnie projektów przy użyciu tylko bibliotek open source. W tym samouczku użyjemy OpenCV w Pythonie do wykrywania obiektów w kształcie okręgów w strumieniu z kamery internetowej i zastąpimy je 3D Earth w Three.js w oknie przeglądarki, jednocześnie używając WebSockets do połączenia tego wszystkiego.

Chcemy ściśle oddzielić front-end i back-end, aby można go było ponownie wykorzystać. W rzeczywistej aplikacji moglibyśmy napisać front-end w Unity, Unreal Engine lub Blenderze, aby wyglądał naprawdę ładnie. Interfejs przeglądarki jest najłatwiejszy do zaimplementowania i powinien działać na prawie każdej możliwej konfiguracji.

Aby uprościć sprawę, podzielimy aplikację na trzy mniejsze części:

  1. Back-end Pythona z OpenCV OpenCV odczyta strumień z kamery internetowej i otworzy wiele okien z obrazem z kamery po przejściu go przez wiele filtrów, aby ułatwić debugowanie i dać nam mały wgląd w to, co faktycznie widzi algorytm wykrywania okręgów. Wynikiem tej części będą tylko współrzędne 2D i promień wykrytego okręgu.
  2. Interfejs JavaScript z Three.js w przeglądarce Implementacja biblioteki Three.js krok po kroku do renderowania Ziemi z teksturą i obracającym się wokół niej księżycem. Najciekawszą rzeczą będzie mapowanie współrzędnych ekranu 2D do świata 3D. Przybliżymy również współrzędne i promień, aby zwiększyć dokładność OpenCV.
  3. WebSockets zarówno w interfejsie, jak i zapleczu Zaplecze z serwerem WebSockets będzie okresowo wysyłać wiadomości z wykrytymi współrzędnymi okręgu i promieniami do klienta przeglądarki.
Ostateczny wynik
Więcej po skoku! Kontynuuj czytanie poniżej ↓

1. Back-end Pythona z OpenCV

Naszym pierwszym krokiem będzie po prostu zaimportowanie biblioteki OpenCV w Pythonie i otwarcie okna ze strumieniem na żywo z kamery internetowej.

Będziemy używać najnowszego OpenCV 3.0 (patrz uwagi dotyczące instalacji) z Pythonem 2.7. Należy pamiętać, że instalacja na niektórych systemach może być problematyczna, a oficjalna dokumentacja nie jest zbyt pomocna. Próbowałem się na Mac OS X w wersji 3.0 z MacPorts i plik binarny miał problem z zależnością, więc zamiast tego musiałem przełączyć się na Homebrew. Zauważ również, że niektóre pakiety OpenCV mogą nie być domyślnie dostarczane z wiązaniem Pythona (musisz użyć niektórych opcji wiersza poleceń).

Z Homebrew prowadziłem:

 brew install opencv

To domyślnie instaluje OpenCV z powiązaniami Pythona.

Aby przetestować różne rzeczy, polecam uruchomić Pythona w trybie interaktywnym (uruchom python w CLI bez żadnych argumentów) i napisać import cv2 . Jeśli OpenCV jest poprawnie zainstalowany i ścieżki do powiązań Pythona są poprawne, nie powinno to powodować błędów.

Później użyjemy również numpy Pythona do kilku prostych operacji na macierzach, więc możemy go również teraz zainstalować.

 pip install numpy

Odczytywanie obrazu z kamery

Teraz możemy przetestować kamerę:

 import cv2 capture = cv2.VideoCapture(0) while True: ret, image = capture.read() cv2.imshow('Camera stream', image) if cv2.waitKey(1) & 0xFF == ord('q'): break

Dzięki cv2.VideoCapture(0) uzyskujemy dostęp do kamery o indeksie 0 , który jest domyślny (zazwyczaj jest to kamera wbudowana). Jeśli chcesz użyć innego, wypróbuj liczby większe od zera; jednak nie ma łatwego sposobu, aby wyświetlić listę wszystkich dostępnych kamer z obecną wersją OpenCV.

Kiedy wywołujemy cv2.imshow('Camera stream', image) po raz pierwszy sprawdza, czy nie istnieje okno o tej nazwie i tworzy dla nas nowe z obrazem z kamery. To samo okno zostanie użyte ponownie dla każdej iteracji pętli głównej.

Następnie użyliśmy capture.read() , aby zaczekać i pobrać bieżący obraz z kamery. Ta metoda zwraca również właściwość Boolean ret w przypadku, gdy kamera jest odłączona lub z jakiegoś powodu następna klatka jest niedostępna.

Na koniec mamy cv2.waitKey(1) , który przez 1 milisekundę sprawdza, czy został naciśnięty jakikolwiek klawisz i zwraca swój kod. Tak więc, gdy naciśniemy q , wyłamujemy się z pętli, zamykamy okno, a aplikacja się kończy.

Jeśli to wszystko działa, przeszliśmy najtrudniejszą część aplikacji zaplecza, która polega na uruchomieniu aparatu.

Filtrowanie obrazów z aparatu

Do rzeczywistego wykrywania okręgów użyjemy transformaty Hougha okręgu , która jest zaimplementowana w cv2.HoughCircles() i jest obecnie jedynym algorytmem dostępnym w OpenCV. Ważną rzeczą dla nas jest to, że potrzebuje obrazu w skali szarości jako danych wejściowych i używa wewnętrznego algorytmu wykrywania krawędzi Canny , aby znaleźć krawędzie obrazu. Chcemy móc ręcznie sprawdzić, co widzi algorytm, więc skomponujemy jeden duży obraz z czterech mniejszych obrazów, każdy z zastosowanym innym filtrem.

Detektor krawędzi Canny to algorytm, który przetwarza obraz zazwyczaj w czterech kierunkach (pionowo, poziomo i po dwóch ukośnych) i znajduje krawędzie. Rzeczywiste kroki, które wykonuje ten algorytm, są wyjaśnione bardziej szczegółowo na Wikipedii lub pokrótce w dokumentacji OpenCV.

W przeciwieństwie do dopasowywania wzorców, ten algorytm wykrywa okrągłe kształty, dzięki czemu możemy użyć dowolnych obiektów, które mamy do ręki, które są okrągłe. Użyję pokrywki ze słoika po kawie rozpuszczalnej, a potem kubka z kawą pomarańczową.

Nie musimy pracować z obrazami w pełnym rozmiarze (oczywiście zależy to od rozdzielczości aparatu), więc zmienimy ich rozmiar pomiędzy capture.read() i cv2.imshow do szerokości i wysokości 640px, aby zachować proporcje:

 width, height = image.shape scale = 640.0 / width image = cv2.resize(image, (0,0), fx=scale, fy=scale)

Następnie chcemy przekonwertować go na obraz w skali szarości i zastosować najpierw rozmycie mediany , które usuwa szum i zachowuje krawędzie, a następnie detektor krawędzi Canny, aby zobaczyć, z czym będzie pracował algorytm wykrywania okręgów. Z tego powodu skomponujemy siatkę 2x2 ze wszystkimi czterema podglądami.

 t = 100 # threshold for Canny Edge Detection algorithm grey = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) blured = cv2.medianBlur(grey, 15) # Create 2x2 grid for all previews grid = np.zeros([2*h, 2*w, 3], np.uint8) grid[0:h, 0:w] = image # We need to convert each of them to RGB from greyscaled 8 bit format grid[h:2*h, 0:w] = np.dstack([cv2.Canny(grey, t / 2, t)] * 3) grid[0:h, w:2*w] = np.dstack([blured] * 3) grid[h:2*h, w:2*w] = np.dstack([cv2.Canny(blured, t / 2, t)] * 3) 
Siatka z podglądami. U góry po lewej: nieprzetworzone dane z kamery internetowej; u góry po prawej: skala szarości po rozmyciu mediany; na dole po lewej: w skali szarości i Canny edge; dolny prawy: skala szarości po medianie rozmycia i Canny edge.

Mimo że wykrywacz krawędzi Canny używa rozmycia Gaussa do redukcji szumów, z mojego doświadczenia wynika, że ​​nadal warto używać rozmycia środkowego. Możesz porównać dwa dolne obrazy. Ten po lewej to tylko wykrywanie krawędzi Canny bez żadnego innego filtra. Drugi obraz to również wykrywanie krawędzi Canny, ale tym razem po zastosowaniu rozmycia mediany. Zmniejszyła obiekty w tle, co pomoże w wykrywaniu okręgów.

Wykrywanie okręgów z dużym gradientem

Wewnętrznie OpenCV wykorzystuje bardziej wydajną implementację Hough Circle Transform o nazwie Hough Gradient Method, która wykorzystuje informacje o krawędziach z detektora krawędzi Canny. Metoda gradientu została szczegółowo opisana w książce Learning OpenCV and the Circle Hough Transform na Wikipedii .

Teraz czas na faktyczne wykrycie okręgu:

 sc = 1 # Scale for the algorithm md = 30 # Minimum required distance between two circles # Accumulator threshold for circle detection. Smaller numbers are more # sensitive to false detections but make the detection more tolerant. at = 40 circles = cv2.HoughCircles(blured, cv2.HOUGH_GRADIENT, sc, md, t, at)

Zwraca tablicę wszystkich wykrytych kręgów. Dla uproszczenia zajmiemy się tylko tym pierwszym. Hough Gradient jest dość czuły na naprawdę okrągłe kształty, więc jest mało prawdopodobne, że spowoduje to fałszywe wykrywanie. Jeśli tak, zwiększ parametr at . Dlatego powyżej użyliśmy mediany rozmycia; usunęło więcej szumu, dzięki czemu możemy użyć niższego progu, dzięki czemu wykrywanie jest bardziej odporne na niedokładności i ma mniejszą szansę na wykrycie fałszywych okręgów.

Wypiszemy środek okręgu i jego promień na konsoli, a także narysujemy znaleziony okrąg ze środkiem na obrazie z kamery w osobnym oknie. Później wyślemy go przez WebSocket do przeglądarki. Zauważ, że x , y i radius są podane w pikselach.

 if circles is not None: # We care only about the first circle found. circle = circles[0][0] x, y, radius = int(circle[0]), int(circle[1]), int(circle[2]) print(x, y, radius) # Highlight the circle cv2.circle(image, [x, y], radius, (0, 0, 255), 1) # Draw a dot in the center cv2.circle(image, [x, y], 1, (0, 0, 255), 1)

Spowoduje to wydrukowanie krotek konsoli, takich jak:

 (251, 202, 74) (252, 203, 73) (250, 202, 74) (246, 202, 76) (246, 204, 74) (246, 205, 72) 
Strumień z kamery internetowej z kręgami wykrytymi przy użyciu Hough Gradient.

Jak widać na tej animacji, w ogóle nie udało się znaleźć żadnych kręgów. Mój wbudowany aparat ma tylko 15 kl./s i kiedy szybko poruszam ręką, obraz jest rozmazany, więc nie znajduje krawędzi okręgów, nawet po nałożeniu filtrów.

Pod koniec tego artykułu wrócimy do tego problemu i dużo porozmawiamy o ustawieniach kamery i wyborze algorytmu wykrywania, ale już teraz możemy powiedzieć, że mimo że moja konfiguracja jest bardzo zła (tylko 15fps, słabe oświetlenie, dużo szumu w tle, obiekt ma niski kontrast), wynik jest w miarę dobry.

To wszystko na teraz. Mamy współrzędne x i y oraz radius w pikselach okręgu znalezionego na obrazie z kamery internetowej.

Możesz zobaczyć pełny kod źródłowy tej części na gist.github.com.

2. JavaScript Front-End z Three.js w przeglądarkach

Część front-endowa jest oparta na bibliotece Three.js (wersja r72). Zaczniemy od stworzenia obracającej się, teksturowanej kuli reprezentującej Ziemię na środku ekranu, a następnie dodamy księżyc obracający się wokół niej. Na koniec zmapujemy współrzędne myszy ekranowej 2D do przestrzeni 3D.

Nasza strona HTML będzie składać się tylko z jednego elementu <canvas> . zobacz index.html na gist.github.com.

Tworzenie Ziemi

JavaScript będzie nieco dłuższy, ale zostanie podzielony na wiele funkcji inicjujących, z których każda ma jeden cel. Tekstury Ziemi i Księżyca pochodzą ze strony planetpixelemporium.com. Pamiętaj, że podczas ładowania tekstur stosowane są reguły CORS.

 var scene, camera, renderer, light, earthMesh, earthRotY = 0; function initScene(width, height) { scene = new THREE.Scene(); // Setup cameta with 45 deg field of view and same aspect ratio camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000); // Set the camera to 400 units along `z` axis camera.position.set(0, 0, 400); renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setSize(width, height); renderer.shadowMap.enabled = true; document.body.appendChild(renderer.domElement); } function initLight() { light = new THREE.SpotLight(0xffffff); // Position the light slightly to a side to make shadows look better. light.position.set(400, 100, 1000); light.castShadow = true; scene.add(light); } function initEarth() { // Load Earth texture and create material from it var earthMaterial = new THREE.MeshLambertMaterial({ map: THREE.ImageUtils.loadTexture("/images/earthmap1k.jpg"), }); // Create a sphere 25 units in radius and 16 segments // both horizontally and vertically. var earthGeometry = new THREE.SphereGeometry(25, 16, 16); earthMesh = new THREE.Mesh(earthGeometry, earthMaterial); earthMesh.receiveShadow = true; earthMesh.castShadow = true; // Add Earth to the scene scene.add(earthMesh); } // Update position of objects in the scene function update() { earthRotY += 0.007; earthMesh.rotation.y = earthRotY; } // Redraw entire scene function render() { update(); renderer.setClearColor(0x000000, 0); renderer.render(scene, camera); // Schedule another frame requestAnimationFrame(render); } document.addEventListener('DOMContentLoaded', function(e) { // Initialize everything and start rendering initScene(window.innerWidth, window.innerHeight); initEarth(); initLight(); // Start rendering the scene requestAnimationFrame(render); });

Zobacz demo na żywo tutaj.

threejs-spinning-earth
Kula teksturowana z Three.js. (Uznanie tekstury Ziemi)

Były to głównie podstawowe rzeczy w Three.js. Nazwy obiektów i metod są oczywiste (takie jak receiveShadow lub castShadow ), ale jeśli nigdy wcześniej ich nie używałeś, zdecydowanie polecam zajrzeć do samouczków Lee Stemkoskiego.

Opcjonalnie moglibyśmy również narysować oś na środku ekranu, aby pomóc nam w układzie współrzędnych.

 var axes = new THREE.AxisHelper(60); axes.position.set(0, 0, 0); scene.add(axes);

Dodawanie Księżyca

Tworzenie księżyca będzie bardzo podobne. Główna różnica polega na tym, że musimy ustalić położenie księżyca względem Ziemi.

 function initMoon() { // The same as initEarth() with just different texture } // Update position of objects in the scene function update() { // Update Earth position // ... // Update Moon position moonRotY += 0.005; radY += 0.03; radZ += 0.0005; // Calculate position on a sphere x = moonDist * Math.cos(radZ) * Math.sin(radY); y = moonDist * Math.sin(radZ) * Math.sin(radY); z = moonDist * Math.cos(radY); var pos = earthMesh.position; // We can keep `z` as is because we're not moving the Earth // along z axis. moonMesh.position.set(x + earthMesh.pos.x, y + earthMesh.pos.y, z); moonMesh.rotation.y = moonRotY; }

Zobacz demo na żywo tutaj.

Ziemia i Księżyc z Three.js. (Uznanie za tekstury Ziemi i Księżyca)

Mapowanie współrzędnych 2D do świata 3D

Jak dotąd wszystko jest dość oczywiste. Najbardziej interesującą częścią będzie to, jak ukryć współrzędne ekranu 2D pochodzące z OpenCV (patrz dane wyjściowe wykrywania kołowego powyżej) w świecie 3D? Kiedy definiowaliśmy promienie i pozycje w Three.js, użyliśmy niektórych jednostek, ale nie mają one nic wspólnego z rzeczywistymi pikselami ekranu. W rzeczywistości wymiary wszystkiego, co widzimy w scenie, w dużym stopniu zależą od ustawień aparatu (takich jak proporcje lub pole widzenia).

Z tego powodu stworzymy płaski obiekt, który będzie wystarczająco duży, aby pokryć całą scenę ze środkiem w [0,0,0] . W celach demonstracyjnych zmapujemy współrzędne myszy 2D do położenia Ziemi w 3D ze stałą z Z. Innymi słowy, skonwertujemy tylko x i y i nie będziemy się martwić o z , czyli odległość od obiektu do naszego aparatu.

Przekształcimy pozycje na ekranie myszy na zakres od -1.0 do +1.0 ze środkiem w [0,0] , ponieważ musimy pracować ze znormalizowanymi wektorami.

Później użyjemy tej dokładnej techniki, aby odwzorować pozycję wykrytego okręgu w 3D, a także dopasować rozmiar okręgu od 2D do 3D.

 var mouse = {}; function initPlane() { // The plane needs to be large to always cover entire scene var tmpGeometry = new THREE.PlaneGeometry(1000, 1000, 1, 1); tmpGeometry.position = new THREE.Vector3(0, 0, 0); var tmpMesh = new THREE.Mesh(tmpGeometry); } function onDocumentMouseMove(event) { // Current mouse position with [0,0] in the center of the window // and ranging from -1.0 to +1.0 with `y` axis inverted. mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = - (event.clientY / window.innerHeight) * 2 + 1; } function update() { // ... the rest of the function // We need mouse x and y coordinates to set vector's direction var vector = new THREE.Vector3(mouse.x, mouse.y, 0.0); // Unproject camera distortion (fov, aspect ratio) vector.unproject(camera); var norm = vector.sub(camera.position).normalize(); // Cast a line from our camera to the tmpMesh and see where these // two intersect. That's our 2D position in 3D coordinates. var ray = new THREE.Raycaster(camera.position, norm); var intersects = ray.intersectObject(tmpMesh); earthMesh.position.x = intersects[0].point.x; earthMesh.position.y = intersects[0].point.y; }

Zobacz demo na żywo tutaj.

threejs-spinning-earth
Pozycja Ziemi w 3D odwzorowana na pozycję myszy 2D. (Uznanie za tekstury Ziemi i Księżyca)

Ponieważ sprawdzamy skrzyżowanie z samolotem, wiemy, że zawsze będzie tylko jedno.

To wszystko w tej części. Pod koniec następnej części dodamy również WebSockets i element <video> z naszym strumieniem kamery, który zostanie nałożony na scenę 3D w Three.js.

3. WebSockets zarówno na froncie, jak i zapleczu

Możemy zacząć od implementacji WebSockets w zapleczu Pythona, instalując biblioteki simple-websocket-server . Istnieje wiele różnych bibliotek, takich jak Tornado czy Autobahn. Użyjemy simple-websocket-server ponieważ jest bardzo łatwy w użyciu i nie ma żadnych zależności.

 pip install git+https://github.com/dpallot/simple-websocket-server.git

Uruchomimy serwer WebSocket w osobnym wątku i będziemy śledzić wszystkich podłączonych klientów.

 from SimpleWebSocketServer import SimpleWebSocketServer, WebSocket clients = [], server = None class SimpleWSServer(WebSocket): def handleConnected(self): clients.append(self) def handleClose(self): clients.remove(self) def run_server(): global server server = SimpleWebSocketServer(', 9000, SimpleWSServer, selectInterval=(1000.0 / 15) / 1000) server.serveforever() t = threading.Thread(target=run_server) t.start() # The rest of the OpenCV code ...

Użyliśmy parametru selectInterval w konstruktorze serwera, aby okresowo sprawdzał, czy nie ma żadnych oczekujących wiadomości. Serwer wysyła komunikaty tylko wtedy, gdy odbiera dane od klientów lub musi zapętlić się w głównym wątku. Nie możemy pozwolić, aby blokował główny wątek, ponieważ OpenCV również tego potrzebuje. Ponieważ wiemy, że kamera działa tylko w 15fps, możemy wykorzystać ten sam interwał na serwerze WebSocket.

Następnie, po wykryciu okręgów, możemy iterować wszystkich podłączonych klientów i wysyłać aktualną pozycję i promień względem rozmiaru obrazu.

 for client in clients: msg = json.dumps({'x': x / w, 'y': y / h, 'radius': radius / w}) client.sendMessage(unicode(msg))

Możesz zobaczyć pełny kod źródłowy serwera na gist.github.com.

Część JavaScript będzie naśladować to samo zachowanie, co w przypadku pozycji myszy. Będziemy również śledzić kilka wiadomości i obliczać średnią wartość dla każdej osi i promienia, aby poprawić dokładność.

 var history = []; var ws = new WebSocket('ws://localhost:9000'); ws.onopen = function() { console.log('onopen'); }; ws.onmessage = function (event) { var m = JSON.parse(event.data); history.push({ x: mx * 2 - 1, y: -my * 2 + 1, radius: m.radius}); // ... rest of the function. };

Zamiast ustawiać pozycję Ziemi na moją aktualną pozycję myszy, użyjemy zmiennej msgHistory .

Prawdopodobnie nie jest konieczne wklejanie całego kodu tutaj, więc zachęcamy do zapoznania się ze szczegółami implementacji na gist.gihtub.com.

Następnie dodaj jeden element <video> ze strumieniem z kamery internetowej wypełniającym całe okno, które zostanie nałożone na naszą scenę 3D z przezroczystym tłem.

 var videoElm = document.querySelector('video'); // Make sure the video fits the window. var constrains = { video: { mandatory: { minWidth: window.innerWidth }}}; if (navigator.getUserMedia) { navigator.getUserMedia(constrains, function(stream) { videoElm.src = window.URL.createObjectURL(stream); // When the webcam stream is ready get it's dimensions. videoElm.oncanplay = function() { init(videoElm.clientWidth, videoElm.clientHeight); // Init everything ... requestAnimationFrame(render); } }, function() {}); }

Wynik końcowy:

Ostateczna aplikacja z mapą Earth na współrzędne znalezione przez OpenCV.

Aby szybko podsumować, co zrobiliśmy i co pokazuje powyższy film:

  1. Back-end Pythona uruchamia serwer WebSocket.
  2. Serwer wykrywa krąg za pomocą OpenCV ze strumienia kamery internetowej.
  3. Klient JavaScript wyświetla ten sam strumień z kamery internetowej za pomocą elementu <video> .
  4. Klient renderuje scenę 3D za pomocą Three.js.
  5. Klient łączy się z serwerem za pomocą protokołu WebSocket i otrzymuje pozycję okręgu oraz promień.

Rzeczywisty kod użyty do tego demo jest dostępny na GitHub. Jest nieco bardziej wyrafinowany, a także interpoluje współrzędne między dwiema wiadomościami z zaplecza, ponieważ strumień z kamery internetowej działa tylko przy 15 klatkach na sekundę, podczas gdy scena 3D jest renderowana z szybkością 60 klatek na sekundę. Możesz zobaczyć oryginalny film na YouTube.

Zastrzeżenia

Warto zwrócić uwagę na kilka ustaleń:

Wykrywanie okręgów nie jest idealne

Fajnie, że działa z każdym okrągłym obiektem, ale jest bardzo wrażliwy na szum i deformację obrazu, chociaż jak widać powyżej nasz wynik jest całkiem niezły. Ponadto prawdopodobnie nie ma dostępnych praktycznych przykładów wykrywania okręgów poza najbardziej podstawowym zastosowaniem. Być może lepiej byłoby użyć wykrywania elipsy, ale nie jest to obecnie zaimplementowane w OpenCV.

Wszystko zależy od Twojej konfiguracji

Wbudowane kamery internetowe są na ogół dość złe. 15 kl./s to za mało, a samo zwiększenie go do 30 kl./s znacznie zmniejsza rozmycie ruchu i sprawia, że ​​wykrywanie jest bardziej niezawodne. Możemy podzielić ten punkt na cztery dodatkowe punkty:

  • Zniekształcenia kamery
    Wiele kamer wprowadza pewne zniekształcenia obrazu, najczęściej efekt rybiego oka, który ma istotny wpływ na wykrywanie kształtu. Dokumentacja OpenCV zawiera bardzo prosty samouczek na temat zmniejszania zniekształceń poprzez kalibrację aparatu.
  • Nie ma oficjalnej listy urządzeń obsługiwanych przez OpenCV
    Nawet jeśli masz już dobry aparat, może nie działać z OpenCV bez dalszych wyjaśnień. Czytałem też o ludziach używających innej biblioteki do przechwytywania obrazu z kamery (np. libdc1394 dla kamer opartych na IEEE 1394), a następnie używających OpenCV tylko do przetwarzania obrazów. Menedżer pakietów Brew pozwala skompilować OpenCV bezpośrednio z obsługą libdc1394.
  • Niektóre aparaty działają lepiej z OpenCV niż inne
    Jeśli masz szczęście, możesz ustawić niektóre opcje aparatu, takie jak klatki na sekundę, bezpośrednio w aparacie, ale może to również nie mieć żadnego efektu, jeśli OpenCV nie jest przyjazny dla twojego urządzenia. Znowu bez żadnego wyjaśnienia.
  • Wszystkie parametry zależą od rzeczywistego użytkowania
    W przypadku użycia w rzeczywistej instalacji, zdecydowanie zaleca się przetestowanie algorytmów i filtrów w rzeczywistym środowisku, ponieważ takie elementy jak światła, kolor tła lub wybór obiektu mają znaczący wpływ na wynik. Obejmuje to również cienie ze światła dziennego, ludzi stojących i tak dalej.

Dopasowywanie wzorców jest zwykle lepszym wyborem

Jeśli zobaczysz jakąkolwiek rozszerzoną rzeczywistość stosowaną w praktyce, prawdopodobnie będzie ona oparta na dopasowaniu wzorców. Jest ogólnie bardziej niezawodny i nie jest tak dotknięty problemami opisanymi powyżej.

Filtry są kluczowe

Myślę, że prawidłowe użycie filtrów wymaga pewnego doświadczenia i zawsze odrobiny magii. Czas przetwarzania większości filtrów zależy od ich parametrów, chociaż w OpenCV 3.0 niektóre z nich są już przepisane w CUDA C (język podobny do C do wysoce równoległego programowania z kartami graficznymi NVIDIA), co zapewnia znaczną poprawę wydajności.

Filtruj dane z OpenCV

Widzieliśmy, że wykrywanie okręgów ma pewne niedokładności: czasami nie znajduje żadnego okręgu lub wykrywa niewłaściwy promień. Aby zminimalizować ten rodzaj błędu, warto byłoby zastosować bardziej wyrafinowaną metodę poprawy dokładności. W naszym przykładzie użyliśmy mediany dla x , y i radius , co jest bardzo proste. Powszechnie stosowanym filtrem z dobrymi wynikami jest filtr Kalmana, używany przez autopiloty dronów w celu zmniejszenia niedokładności pochodzących z czujników. Jednak jego implementacja nie jest tak prosta, jak użycie math.mean() z https://mathjs.org.

Wniosek

Po raz pierwszy zobaczyłem podobną aplikację w Narodowym Muzeum Historii Naturalnej w Madrycie dwa lata temu i zastanawiałem się, jak trudno byłoby zrobić coś podobnego.

Moją główną ideą stojącą za tym demo było użycie narzędzi, które są powszechne w sieci (takich jak WebSockets i Three.js) i nie wymagają żadnych wymagań wstępnych, więc każdy może zacząć z nich korzystać od razu. Dlatego chciałem użyć tylko wykrywania okręgów, a nie dopasowywania wzorców, co wymagałoby wydrukowania lub posiadania jakiegoś konkretnego obiektu ze świata rzeczywistego.

Muszę powiedzieć, że mocno nie doceniłem rzeczywistych wymagań dotyczących aparatu. Wysoka liczba klatek na sekundę i dobre oświetlenie są ważniejsze niż rozdzielczość. Nie spodziewałem się też, że niezgodność aparatu z OpenCV będzie problemem.