Realidad aumentada simple con OpenCV, Three.js y WebSockets

 

 

 

  • Patrones de diseño de interfaces inteligentes, vídeo de 10h + formación UX
  • SmashingConf UX y diseño, Amberes 2024

  • Índice
    1. 1. Back-End de Python con OpenCV
      1. Leyendo la imagen de la cámara
      2. Filtrado de imágenes de la cámara
    2. 2. Interfaz de JavaScript con Three.js en navegadores
      1. Creando la Tierra
      2. Añadiendo la luna
      3. 3. WebSockets In Both Front-End And Back-End
    3. Caveats
      1. Circle Detection Isn’t Ideal
      2. Everything Depends On Your Setup
      3. Pattern Matching Is Usually A Better Choice
      4. Filters Are Crucial
      5. Filter Data From OpenCV
    4. Conclusion
      1. Further Reading

    Es posible realizar proyectos de realidad aumentada visualmente impresionantes utilizando únicamente bibliotecas de código abierto. En este tutorial, Martin Sikora utilizará OpenCV en Python para detectar objetos en forma de círculo en una transmisión de cámara web y los reemplazará con 3D Earth en Three.js en una ventana del navegador mientras usa WebSockets para unir todo esto. Su idea central detrás de esta demostración era utilizar herramientas que son comunes en la web y que no requieren ningún requisito previo para que cualquiera pueda comenzar a usarlas de inmediato. Es por eso que Martin quería usar solo la detección de círculos y no la coincidencia de patrones, lo que requeriría imprimir o tener algún objeto particular del mundo real.

     

    Generalmente se considera que la realidad aumentada es muy difícil de crear. Sin embargo, es posible realizar proyectos visualmente impresionantes utilizando únicamente bibliotecas de código abierto. En este tutorial, usaremos OpenCV en Python para detectar objetos en forma de círculo en una transmisión de cámara web y los reemplazaremos con 3D Earth en Three.js en una ventana del navegador mientras usamos WebSockets para unir todo esto.

    Queremos separar estrictamente el front-end y el back-end para que sean reutilizables. En una aplicación del mundo real, podríamos escribir el front-end en Unity, Unreal Engine o Blender, por ejemplo, para que se vea realmente bien. La interfaz del navegador es la más fácil de implementar y debería funcionar en casi todas las configuraciones posibles.

    Para simplificar las cosas, dividiremos la aplicación en tres partes más pequeñas:

    1. El back-end de Python con OpenCV OpenCV leerá la transmisión de la cámara web y abrirá múltiples ventanas con la imagen de la cámara después de pasarla por múltiples filtros para facilitar la depuración y brindarnos una pequeña idea de lo que realmente ve el algoritmo de detección de círculos. La salida de esta parte será solo las coordenadas 2D y el radio del círculo detectado.
    2. Interfaz de JavaScript con Three.js en un navegador Implementación paso a paso de la biblioteca Three.js para representar la Tierra texturizada con la luna girando a su alrededor. Lo más interesante aquí será mapear las coordenadas de la pantalla 2D en el mundo 3D. También aproximaremos las coordenadas y el radio para aumentar la precisión de OpenCV.
    3. WebSockets tanto en el front-end como en el back-end El back-end con el servidor WebSockets enviará periódicamente mensajes con las coordenadas y radios del círculo detectado al cliente del navegador.

    , la nueva guía de Steven Hoober sobre diseño para dispositivos móviles con pautas probadas, universales y centradas en el ser humano. 400 páginas , repletas de investigaciones de usuarios en profundidad y mejores prácticas .

    Saltar a la tabla de contenidos ↬

    1. Back-End de Python con OpenCV

    Nuestro primer paso será simplemente importar la biblioteca OpenCV en Python y abrir una ventana con una transmisión de cámara web en vivo.

    Usaremos el OpenCV 3.0 más nuevo ( consulte las notas de instalación ) con Python 2.7. Tenga en cuenta que la instalación en algunos sistemas puede ser problemática y la documentación oficial no es muy útil. Lo probé en Mac OS X versión 3.0 desde MacPorts y el binario tenía un problema de dependencia, por lo que tuve que cambiar a Homebrew . También tenga en cuenta que es posible que algunos paquetes OpenCV no vengan con el enlace Python de forma predeterminada (debe usar algunas opciones de línea de comando).

    Con Homebrew corrí:

    brew install opencv

    Esto instala OpenCV con enlaces de Python de forma predeterminada.

    Solo para probar las cosas, te recomiendo que ejecutes Python en modo interactivo (ejecutarlo pythonen CLI sin ningún argumento) y escribir import cv2. Si OpenCV está instalado correctamente y las rutas a los enlaces de Python son correctas, no debería generar ningún error.

     

    Más adelante, también usaremos Python numpypara algunas operaciones simples con matrices, por lo que también podemos instalarlo ahora.

    pip install numpy

    Leyendo la imagen de la cámara

    Ahora podemos probar la cámara:

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

    Con esto cv2.VideoCapture(0)obtenemos acceso a la cámara en el índice, 0que es la predeterminada (generalmente la cámara incorporada). Si desea utilizar uno diferente, pruebe con números mayores que cero; sin embargo, no existe una manera fácil de enumerar todas las cámaras disponibles con la versión actual de OpenCV.

    Cuando llamamos cv2.imshow('Camera stream', image)por primera vez comprueba que no existe ninguna ventana con este nombre y nos crea una nueva con una imagen de la cámara. Se reutilizará la misma ventana para cada iteración del bucle principal.

    Luego solíamos capture.read()esperar y tomar la imagen actual de la cámara. Este método también devuelve una propiedad booleana reten caso de que la cámara esté desconectada o el siguiente fotograma no esté disponible por algún motivo.

    Al final tenemos cv2.waitKey(1)que comprueba durante 1 milisegundo si se pulsa alguna tecla y devuelve su código. Entonces, cuando presionamos qsalimos del bucle, cerramos la ventana y la aplicación finalizará.

    Si todo esto funciona, pasamos la parte más difícil de la aplicación de fondo, que es hacer que la cámara funcione.

    Filtrado de imágenes de la cámara

    Para la detección de círculos real, usaremos Circle Hough Transform , que se implementa en cv2.HoughCircles()el método y en este momento es el único algoritmo disponible en OpenCV. Lo importante para nosotros es que necesita una imagen en escala de grises como entrada y utiliza el algoritmo detector de bordes Canny en su interior para encontrar bordes en la imagen. Queremos poder verificar manualmente lo que ve el algoritmo, por lo que componeremos una imagen grande a partir de cuatro imágenes más pequeñas, cada una con un filtro diferente aplicado.

    El detector de bordes Canny es un algoritmo que procesa la imagen normalmente en cuatro direcciones (vertical, horizontal y dos diagonales) y encuentra bordes. Los pasos reales que realiza este algoritmo se explican con mayor detalle en Wikipedia o brevemente en los documentos de OpenCV .

    A diferencia de la coincidencia de patrones, este algoritmo detecta formas circulares, por lo que podemos usar cualquier objeto que tengamos a mano que sea circular. Voy a usar una tapa de un frasco de café instantáneo y luego una taza de café de color naranja.

    No necesitamos trabajar con imágenes de tamaño completo (depende de la resolución de su cámara, por supuesto), por lo que cambiaremos su tamaño entre capture.read()640 cv2.imshowpx de ancho y alto en consecuencia para mantener la relación de aspecto:

     

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

    Luego queremos convertirla a una imagen en escala de grises y aplicar primero el desenfoque medio que elimina el ruido y retiene los bordes, y luego el detector de bordes Canny para ver con qué va a funcionar el algoritmo de detección de círculos. Por este motivo, componeremos una cuadrícula de 2x2 con las cuatro vistas previas.

    t = 100 # threshold for Canny Edge Detection algorithmgrey = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)blured = cv2.medianBlur(grey, 15)# Create 2x2 grid for all previewsgrid = 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 formatgrid[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)

    and the Circle Hough Transform en Wikipedia .

    Ahora es el momento de la detección del círculo real:

    sc = 1 # Scale for the algorithmmd = 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 = 40circles = cv2.HoughCircles(blured, cv2.HOUGH_GRADIENT, sc, md, t, at)

    Esto devuelve una matriz de todos los círculos detectados. En aras de la simplicidad, nos ocuparemos sólo del primero. Hough Gradient es bastante sensible a formas realmente circulares, por lo que es poco probable que esto dé como resultado detecciones falsas. Si es así, aumente el atparámetro. Es por eso que usamos el desenfoque medio arriba; eliminó más ruido para que podamos usar un umbral más bajo, lo que hace que la detección sea más tolerante a las imprecisiones y con menos posibilidades de detectar círculos falsos.

    Imprimiremos el centro del círculo y su radio en la consola y también dibujaremos el círculo encontrado con su centro en la imagen de la cámara en una ventana separada. Posteriormente lo enviaremos vía WebSocket al navegador. Tenga en cuenta que xy están todos en píxeles.yradius

    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)

    Esto imprimirá en la consola tuplas como:

    (251, 202, 74)(252, 203, 73)(250, 202, 74)(246, 202, 76)(246, 204, 74)(246, 205, 72)

    .

    2. Interfaz de JavaScript con Three.js en navegadores

    La parte del front-end se basa en la biblioteca Three.js (versión r72). Comenzaremos simplemente creando una esfera texturizada giratoria que represente la Tierra en el centro de la pantalla, luego agregaremos la luna girando a su alrededor. Al final, asignaremos las coordenadas 2D del mouse de la pantalla al espacio 3D. Foro ciclismo

    Nuestra página HTML constará de un solo canvaselemento. consulte index.html en gist.github.com .

     

    Creando la Tierra

    JavaScript será un poco más largo, pero está dividido en múltiples funciones de inicialización, cada una de las cuales tiene un único propósito. Las texturas de la Tierra y la Luna provienen de planetpixelemporium.com . Tenga en cuenta que al cargar texturas, se aplican las reglas 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 scenefunction update() { earthRotY += 0.007; earthMesh.rotation.y = earthRotY;}// Redraw entire scenefunction 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);});

    Vea una demostración en vivo aquí.

    Esto era principalmente cosas básicas de Three.js. Los nombres de objetos y métodos se explican por sí mismos (como receiveShadowo castShadow), pero si nunca los ha usado antes, le recomiendo que consulte los tutoriales de Lee Stemkoski .

    Opcionalmente también podríamos dibujar un eje en el centro de la pantalla para ayudarnos con el sistema de coordenadas.

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

    Añadiendo la luna

    Creating the moon is going to be very similar. The main difference is that we need to set the moon’s position relative to Earth.

    function initMoon() { // The same as initEarth() with just different texture}// Update position of objects in the scenefunction 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;}

    See a live demo here.

     

    Since we’re checking the intersection with a plane, we know there’s always going to be only one.

    That’s all for this part. At the end of the next part we’ll also add WebSockets and a video element with our camera stream that will be overlaid by the 3D scene in Three.js.

    3. WebSockets In Both Front-End And Back-End

    We can start by implementing WebSockets in the Python back-end by installing simple-websocket-server libraries. There’re many different libraries like Tornado or Autobahn. We’ll use simple-websocket-server because it’s very easy to use and has no dependecies.

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

    We’ll run the WebSocket server in a separate thread and keep track of all connected clients.

    from SimpleWebSocketServer import SimpleWebSocketServer, WebSocketclients = [], server = Noneclass 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 ...

    We used the selectInterval parameter in the server’s constructor to make it periodically check for any pending messages. The server sends messages only when receiving data from clients, or it needs to sit on the main thread in a loop. We can’t let it block the main thread because OpenCV needs it as well. Since we know that the camera runs only at 15fps we can use the same interval on the WebSocket server.

    Then, after we detect the circles, we can iterate all connected clients and send the current position and radius relative to the image size.

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

    You can see the full source code for the server is on gist.github.com.

    The JavaScript part will mimic the same behavior as we did with mouse position. We’ll also keep track of the few messages and calculate a mean value for each axis and radius to improve accuracy.

    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: m.x * 2 - 1, y: -m.y * 2 + 1, radius: m.radius}); // ... rest of the function.};

    Instead of setting Earth’s position to my current mouse position we’ll use the msgHistory variable.

    It’s probably not necessary to paste the entire code here, so feel free to look at implementation details on gist.gihtub.com.

    Then add one video element with the webcam stream filling the entire window that will be overlaid by our 3D scene with a transparent background.

    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() {});}

    The final result:

     

    .

  • Client connects to the server via WebSocket protocol and receives circle position and radius.
  • The actual code used for this demo is available on GitHub. It’s slightly more sophisticated and also interpolates coordinates between two messages from the back-end because the webcam stream runs only at 15fps while the 3D scene is rendered at 60fps. You can see the original video on YouTube.

    Caveats

    There are some findings worth noting:

    Circle Detection Isn’t Ideal

    It’s great that it works with any circular object but it’s very sensitive to noise and image deformation, although as you can see above our result is pretty good. Also, there are probably no practical examples of circle detection available apart from the most basic usage. It might be better to use ellipse detection but it’s not implemented in OpenCV right now.

    Everything Depends On Your Setup

    Built-in webcams are generally pretty bad. 15fps isn’t enough and just increasing it to 30fps reduces motion blur significantly and makes detection more reliable. We can break down this point into four more points:

    • Camera distortions
      Many cameras introduce some image distortion, most commonly a fish-eye effect which has a significant influence on shape detection. OpenCV’s documentation has a very straightforward tutorial on how to reduce distortion by calibrating your camera.
    • There’s no official list of devices supported by OpenCV
      Even if you already have a good camera it might not work with OpenCV without further explanation. I’ve also read about people using some other library to capture a camera image (like libdc1394 for IEEE 1394-based cameras) and then using OpenCV just to process the images. Brew package manager lets you compile OpenCV directly with libdc1394 support.
    • Some cameras work better with OpenCV than others
      If you’re lucky you can set some camera options like frames per second directly on your camera but it might also have no effect at all if OpenCV isn’t friendly with your device. Again, without any explanation.
    • All parameters depend on a real-world usage
      When used in a real-world installation, it’s highly recommended to test the algorithms and filters in the actual environment because things like lights, background color or object choice have significant effects on the result. This also includes shadows from daylight, people standing around, and so on.

    Pattern Matching Is Usually A Better Choice

    If you see any augmented reality used in practice it’ll be probably based on pattern matching. It’s generally more reliable and not so affected by the problems described above.

    Filters Are Crucial

    I think correct usage of filters requires some experience and always a little magic. The processing time of most filters depends on their parameters, although in OpenCV 3.0 some of them are already rewritten into CUDA C (a C-like language for highly parallel programming with NVIDIA graphics cards) which brings significant performance improvements.

    Filter Data From OpenCV

    We’ve seen that circle detection has some inaccuracies: sometimes it fails to find any circle or it detects the wrong radius. To minimize this type of error it would be worthwhile to implement some more sophisticated method to improve accuracy. In our example we used median for x, y and radius, which is very simple. A commonly used filter with good results is the Kalman filter, used by autopilots for drones to reduce inaccuracy coming from sensors. However, its implementation isn’t as simple as using just math.mean() from https://mathjs.org.

    Conclusion

    I first saw a similar application in the National Museum of Natural History in Madrid two years ago and I wondered how difficult it would be to make something similar.

    My core idea behind this demo was to use tools that are common on the web (like WebSockets and Three.js) and don’t require any prerequisites so anyone can start using them right away. That’s why I wanted to use just circle detection and not pattern matching, which would require to print or have some particular real-world object.

    I need to say that I severely underestimated the actual camera requirements. High frames per second and good lighting are more important than resolution. I also didn’t expect that camera incompatibility with OpenCV would be an issue.

    Further Reading

    • Getting Started With Neon Branching
    • Knip: An Automated Tool For Finding Unused Files, Exports, And Dependencies
    • A High-Level Overview Of Large Language Model Concepts, Use Cases, And Tools
    • How To Build A Real-Time Multiplayer Virtual Reality Game (Part 1)

    (rb, jb, og, ml, mrn)Explora más en

    • Codificación
    • javascript
    • Aplicaciones
    • Realidad aumentada





    Tal vez te puede interesar:

    1. ¿Deberían abrirse los enlaces en ventanas nuevas?
    2. 24 excelentes tutoriales de AJAX
    3. 70 técnicas nuevas y útiles de AJAX y JavaScript
    4. Más de 45 excelentes recursos y repositorios de fragmentos de código

    Realidad aumentada simple con OpenCV, Three.js y WebSockets

    Realidad aumentada simple con OpenCV, Three.js y WebSockets

    Patrones de diseño de interfaces inteligentes, vídeo de 10h + formación UX SmashingConf UX y diseño, Amberes 2024 Índice

    programar

    es

    https://pseint.es/static/images/programar-realidad-aumentada-simple-con-opencv-886-0.jpg

    2024-04-04

     

    Realidad aumentada simple con OpenCV, Three.js y WebSockets
    Realidad aumentada simple con OpenCV, Three.js y WebSockets

    Si crees que alguno de los contenidos (texto, imagenes o multimedia) en esta página infringe tus derechos relativos a propiedad intelectual, marcas registradas o cualquier otro de tus derechos, por favor ponte en contacto con nosotros en el mail [email protected] y retiraremos este contenido inmediatamente

     

     

    Top 20