Fue en 2015. Las gafas de realidad virtual Oculus DK2 recién aparecieron a la venta, el mercado de los juegos de realidad virtual estaba ganando popularidad rápidamente.
Las oportunidades del jugador en tales juegos eran limitadas. Solo se controlaron 6 grados de libertad de movimientos de la cabeza: rotación (con una inercia en las gafas) y movimiento en un pequeño volumen en el campo de visión de una cámara de infrarrojos fijada en el monitor. El proceso del juego consistía en sentarse en una silla con un mando en las manos, rotar la cabeza en distintas direcciones y combatir las náuseas.
No sonaba muy bien, pero vi esto como una oportunidad para hacer algo interesante, usando mi experiencia en el desarrollo de electrónica y mi sed de nuevos proyectos. ¿Cómo se podría mejorar este sistema?
Por supuesto, deshacerse del gamepad, los cables, permiten al jugador moverse libremente en el espacio, ver sus manos y pies, interactuar con el entorno, otros jugadores y objetos interactivos reales.
Lo vi así:
- Llevamos a varios jugadores, nos ponemos gafas de realidad virtual, un portátil y sensores en brazos, piernas y torso.
- Tomamos una habitación que consta de varias habitaciones, pasillos, puertas, la equipamos con un sistema de seguimiento, colgamos sensores y cerraduras magnéticas en las puertas, agregamos varios objetos interactivos y creamos un juego en el que la geometría de una ubicación virtual repite exactamente la geometría de una habitación real.
- Creamos un juego. El juego es una misión multijugador en la que varios jugadores se ponen el equipo y se encuentran en un mundo virtual. En él, se ven a sí mismos, se ven, pueden caminar por el lugar, abrir puertas y resolver juntos los problemas del juego.
Le conté esta idea a mi amigo, quien inesperadamente la aceptó con gran entusiasmo y se ofreció a hacerse cargo de los asuntos organizativos. Así que decidimos confundir la puesta en marcha.
Para implementar la funcionalidad declarada, fue necesario crear dos tecnologías principales:
- un traje que consta de sensores en los brazos, las piernas y el torso que rastrea la posición de las partes del cuerpo del jugador
- un sistema de seguimiento que rastrea jugadores y objetos interactivos en el espacio 3D.
El desarrollo de la segunda tecnología se discutirá en este artículo. Quizás más tarde escriba sobre el primero.
Sistema de rastreo.
Por supuesto, no teníamos el presupuesto para todo esto, así que tuvimos que hacer de todo, desde materiales de desecho. Para la tarea de rastrear a los jugadores en el espacio, decidí usar cámaras ópticas y marcadores LED conectados a las gafas de realidad virtual. No tenía experiencia en tal desarrollo, pero ya escuché algo sobre OpenCV, Python y pensé que podía hacerlo.
Tal como se concibió, si el sistema sabe dónde está ubicada la cámara y cómo está orientada, entonces mediante la posición de la imagen del marcador en el marco, puede determinar la línea recta en el espacio 3D en el que se encuentra este marcador. La intersección de dos de estas líneas da la posición final del marcador.
En consecuencia, las cámaras tenían que fijarse en el techo para que cada punto del espacio fuera visto por al menos dos cámaras (preferiblemente más, para evitar obstruir la vista por los cuerpos de los jugadores). Se necesitaron unas 60 cámaras para cubrir el supuesto local con una superficie de unos 100 metros cuadrados. Elegí las primeras cámaras web USB baratas disponibles en ese momento.
Estas cámaras web deben estar conectadas a algo. Los experimentos han demostrado que al usar cables de extensión USB (al menos baratos), las cámaras comenzaron a fallar. Por lo tanto, decidí dividir las cámaras web en grupos de 8 piezas y pegarlas en las unidades del sistema montadas en el techo. Solo había 10 puertos USB en la computadora de mi casa, por lo que es hora de comenzar a desarrollar un banco de pruebas.
La arquitectura que se me ocurrió es la siguiente:
En cada vaso se cuelga una bola de acrílico mate de una guirnalda con un LED RGB pegado en el interior. Se suponía que había varios jugadores en el juego al mismo tiempo, así que para la identificación decidí separarlos por color: R, G, B, RG, RB, GB, RB. Así es como se veía:
la primera tarea a realizar es escribir un programa para encontrar una pelota en un marco.
Encontrar la pelota en el marco
Tuve que buscar las coordenadas del centro de la bola y su color para la identificación en cada cuadro que venía de la cámara. Suena fácil. Descargo OpenCV para Python, conecto la cámara al usb, escribo un script. Para minimizar el efecto de objetos innecesarios en el encuadre, configuro la exposición y la velocidad de obturación de la cámara al mínimo, y el brillo del LED es alto para obtener puntos brillantes sobre un fondo oscuro. En la primera versión, el algoritmo era el siguiente:
- ( , , – ).
- .
- ( )
Parece funcionar, pero hay matices.
En primer lugar, en una cámara barata, la matriz es bastante ruidosa, lo que conduce a fluctuaciones constantes de los contornos de los grupos binarizados y, en consecuencia, a sacudidas del centro. Es imposible que los jugadores muevan la imagen con gafas de realidad virtual, por lo que este problema tuvo que resolverse. Los intentos de aplicar otros tipos de binarización adaptativa con diferentes parámetros no dieron mucho efecto.
En segundo lugar, la resolución de la cámara es de solo 640 * 480, por lo que a cierta distancia (no muy grande) la bola es visible como un par de píxeles en el marco y el algoritmo de búsqueda de bordes deja de funcionar normalmente.
Tuve que idear un nuevo algoritmo. Se me ocurrió la siguiente idea:
- Convertir la imagen a escala de grises
- Gaussian blur – ,
- ,
Esto funciona mucho mejor, las coordenadas del centro están estacionarias cuando la bola está estacionaria y funciona incluso a una gran distancia de la cámara.
Para asegurarse de que todo esto funcione con 8 cámaras en una computadora, debe realizar una prueba de esfuerzo.
Prueba de carga
Conecto 8 cámaras a mi escritorio, las ordeno para que cada una vea puntos brillantes y ejecuto un script donde el algoritmo descrito funciona en 8 procesos independientes (gracias a la librería de multiprocesamiento Python) y procesa todos los hilos a la vez.
Y ... inmediatamente me encuentro con un fracaso. Las imágenes de la cámara aparecen y desaparecen, la velocidad de fotogramas salta de 0 a 100, una pesadilla. La investigación mostró que algunos de los puertos USB de mi computadora están conectados al mismo bus a través de un concentrador interno, por lo que la velocidad del bus se divide entre varios puertos y ya no es suficiente para la tasa de bits de la cámara. Conectar las cámaras a diferentes puertos de la computadora en diferentes combinaciones mostró que solo tengo 4 buses USB independientes. Tuve que encontrar una placa base con 8 buses, lo cual fue una búsqueda bastante difícil.
Revelación
Surgieron placas base con chipset Intel B85, que admiten hasta 10 buses USB. Pero para trabajar con 10 cámaras, debe volver a compilar OpenCV, porque tiene un límite codificado de 8 fuentes de video (¿coincidencia?)
Continúo la prueba de carga. Esta vez, todas las cámaras están conectadas y emiten flujos normales, pero inmediatamente me encuentro con el siguiente problema: fps bajos. El procesador está cargado al 100% y solo logra procesar de 8 a 10 fotogramas por segundo de cada una de las ocho cámaras web.
Parece que el código debe optimizarse. El cuello de botella resultó ser el desenfoque gaussiano (no es sorprendente, porque necesita convolucionar con una matriz de 9 * 9 para cada píxel del fotograma). Reducir el núcleo no salvó la situación. Tuve que buscar otro método para encontrar los centros de las manchas en los marcos.
La solución se encontró de repente en la función SimpleBlobDetector integrada en OpenCV. Ella hace exactamente lo que necesito y muy rápido. La ventaja se logra debido a la binarización secuencial de la imagen con diferentes umbrales y la búsqueda de contornos. El resultado es un máximo de 30 fps con una carga de CPU inferior al 40%. ¡Prueba de carga aprobada!
Clasificación de color
La siguiente tarea es clasificar el marcador por su color. El valor de color promedio sobre los píxeles puntuales proporciona componentes RGB que son muy inestables y varían mucho según la distancia a la cámara y el brillo del LED. Pero hay una gran solución: la traducción del espacio RGB con HSV (tono, saturación, valor). En esta representación, el píxel en lugar de "rojo", "azul", "verde" se descompone en los componentes "tono", "saturación", "brillo". En este caso, la saturación y el brillo simplemente pueden excluirse y clasificarse solo por tono.
Detalles técnicos
, «» . , . , . «» .
:
:
- (, R – )
- , , . «hue – saturation»
- . , .
- , , . . , , . .. , . , - , , .
Y así, en este momento he aprendido a buscar e identificar marcadores en fotogramas de una gran cantidad de cámaras. Ahora puede pasar a la siguiente etapa: seguimiento en el espacio.
Rastreo
Utilicé un modelo de cámara estenopeica en el que todos los rayos caen sobre la matriz a través de un punto ubicado en la distancia focal de la matriz.
Este modelo transformará las coordenadas bidimensionales de un punto en el marco en ecuaciones tridimensionales de una línea recta en el espacio.
Para rastrear las coordenadas 3D del marcador, necesita obtener al menos dos líneas de intersección en el espacio de diferentes cámaras y encontrar el punto de su intersección. No es difícil ver el marcador con dos cámaras, pero para construir estas líneas se necesita que el sistema sepa todo sobre las cámaras conectadas: dónde cuelgan, en qué ángulos, la distancia focal de cada lente. El problema es que nada de esto se sabe. El cálculo de los parámetros requiere algún tipo de procedimiento de calibración.
Seguimiento de la calibración
En la primera versión, decidí hacer el seguimiento de la calibración lo más primitivo posible.
- Cuelgo el primer bloque de ocho cámaras en el techo, las conecto a una unidad del sistema que cuelga en el mismo lugar, dirijo las cámaras para que cubran el volumen máximo del juego.
- Usando un nivel láser y un telémetro, mido las coordenadas XYZ de todas las cámaras en un solo sistema de coordenadas
- Para calcular las orientaciones y distancias focales de las cámaras, mido las coordenadas de pegatinas especiales. Cuelgo las pegatinas de la siguiente manera:
- En la interfaz para mostrar una imagen de la cámara, dibujo dos puntos. Uno en el centro del marco, otros 200 píxeles a la derecha del centro

- Si miras el marco, estos puntos caen en algún lugar de la pared, el piso o cualquier otro objeto dentro de la habitación. Cuelgo pegatinas de papel en los lugares adecuados y les dibujo puntos con un marcador.
- Mido las coordenadas XYZ de estos puntos usando el mismo nivel y telémetro. En total, para un bloque de ocho cámaras, debe medir las coordenadas de las propias cámaras y dos puntos más para cada una. Aquellos. 24 tripletes de coordenadas. Y debería haber unos diez de esos bloques. Resulta un trabajo largo y aburrido. Pero nada, automatizaré la calibración más tarde.
- Empiezo el proceso de cálculo en base a los datos medidos
- En la interfaz para mostrar una imagen de la cámara, dibujo dos puntos. Uno en el centro del marco, otros 200 píxeles a la derecha del centro
Hay dos sistemas de coordenadas: uno global, asociado a la habitación, y el otro local a cada cámara. En mi algoritmo, el resultado para cada cámara debería ser una matriz de 4 * 4 que contenga su ubicación y orientación, permitiendo que las coordenadas se conviertan de locales a globales.
La idea es la siguiente:
- Tomamos la matriz original con cero rotaciones y desplazamiento.
- , .
- , .
- , . , . . 200 . , .
- (, 200 ).
Seguramente este problema podría resolverse analíticamente, pero por simplicidad usé una solución numérica sobre el descenso de gradientes. No da miedo, porque los cálculos se realizarán una vez que se instalen las cámaras.
Para visualizar los resultados de la calibración, hice una interfaz 2D con un mapa, en el que el script dibuja las etiquetas de las cámaras y las direcciones en las que ven los marcadores. Los triángulos representan las orientaciones de la cámara y los ángulos de visión.
Prueba del seguimiento
Puede comenzar a ejecutar la visualización, que mostrará si las orientaciones de la cámara se han identificado correctamente y si los fotogramas se interpretan correctamente. Idealmente, las líneas que provienen de los iconos de la cámara deberían cruzarse en un punto.
Esto es lo que pasó:
Parece la verdad, pero la precisión claramente podría ser mayor. La primera razón de imperfección que me vino a la mente es la distorsión en las lentes de las cámaras. Esto significa que estas distorsiones deben compensarse de alguna manera.
Calibración de la cámara
La cámara ideal solo tiene un parámetro importante para mí: la distancia focal. La curva de la cámara real también debe tener en cuenta la distorsión de la lente y el desplazamiento del centro de la matriz.
Para medir estos parámetros, existe un procedimiento de calibración estándar, durante el cual se toman un conjunto de fotografías de un tablero de ajedrez con una cámara medida, en el que los ángulos entre los cuadrados se reconocen con precisión de subpíxeles.
El resultado de la calibración es una matriz que contiene las distancias focales a lo largo de dos ejes y el desplazamiento de la matriz con respecto al centro óptico. Todo esto se mide en píxeles.
Además de un vector de coeficientes de distorsión
que le permite compensar las distorsiones de la lente mediante transformaciones de coordenadas de píxeles.
Al aplicar transformaciones con estos coeficientes a las coordenadas del marcador en el marco, puede llevar el sistema a un modelo de cámara estenopeica ideal.
Ejecución de una nueva prueba de seguimiento: ¡
mucho mejor! Se ve tan bien que incluso parece funcionar.
Pero el proceso de calibración resulta ser muy lúgubre: mide directamente las coordenadas de cada cámara, comienza a mostrar una imagen de cada cámara, cuelga pegatinas, mide las coordenadas de cada pegatina, escribe los resultados en una tabla y calibra las lentes. Todo esto llevó un par de días y un kilogramo de nervios. Decidí ocuparme del seguimiento y escribir algo más automatizado.
Calcular las coordenadas del marcador
Y así, obtuve un montón de líneas rectas, esparcidas en el espacio, en las intersecciones de las cuales debería haber marcadores. Solo las líneas rectas en el espacio no se cruzan realmente, sino que se cruzan, es decir, pasar a cierta distancia el uno del otro. Mi tarea es encontrar el punto lo más cerca posible de ambas líneas rectas. Hablando formalmente, necesitas encontrar el punto medio del segmento que es perpendicular a ambas líneas.
La longitud del segmento AB también es útil, porque refleja la "calidad" del resultado obtenido. Cuanto más corto sea, cuanto más cerca estén las líneas rectas, mejor será el resultado.
Luego escribí un algoritmo de seguimiento que calcula las intersecciones de líneas en pares (dentro del mismo color, de cámaras que están a una distancia suficiente entre sí), busca lo mejor y lo usa como coordenadas de marcador. En los siguientes fotogramas, intenta utilizar el mismo par de cámaras para evitar un salto de coordenadas al cambiar al seguimiento con otras cámaras.
En paralelo, mientras desarrollaba un traje con sensores, descubrí un fenómeno extraño. Todos los sensores mostraron diferentes valores del ángulo de guiñada (dirección en el plano horizontal), como si cada uno tuviera su propio norte. En primer lugar, fue útil comprobar si me equivoqué en los algoritmos de filtrado de datos o en el diseño del tablero, pero no encontré nada. Entonces decidí mirar los datos sin procesar del magnetómetro y vi el problema.
¡El campo magnético de nuestra habitación se dirigió VERTICALMENTE HACIA ABAJO! Aparentemente, esto se debe al hierro en la estructura del edificio.
Pero las gafas de realidad virtual también utilizan un magnetómetro. ¿Por qué no tienen este efecto? Voy a comprobar. Resultó que también tiene gafas ... Si te quedas quieto, puedes ver cómo el mundo virtual gira lenta pero seguramente a tu alrededor en una dirección aleatoria. En 10 minutos, sale casi 180 grados. En nuestro juego, esto conducirá inevitablemente a una desincronización entre las realidades virtuales y reales y a los cristales rotos contra las paredes.
Parece que además de las coordenadas de las gafas, tendrás que determinar su dirección en el plano horizontal. La solución se sugiere a sí misma: poner no uno, sino dos marcadores idénticos en los vasos. Le permitirá determinar la dirección con una precisión de 180 grados, pero teniendo en cuenta la presencia de sensores inerciales incorporados, esto es suficiente.
El sistema en su conjunto funcionó, aunque con pequeñas jambas. Pero se tomó la decisión de lanzar la misión, que estaba a punto de ser completada por nuestro desarrollador de desarrollo de juegos que se unió a nuestro mini-equipo. Se destruyó toda el área de juego, se instalaron puertas con sensores y cerraduras magnéticas, y se realizaron dos objetos interactivos:
Los jugadores se pusieron anteojos, trajes y mochilas de computadora y entraron al área de juego. Las coordenadas de seguimiento se les enviaron a través de wi-fi y se utilizaron para posicionar al personaje virtual. Todo funcionó bastante bien, los visitantes están contentos. Lo más agradable fue ver el horror y los gritos de visitantes especialmente impresionables en los momentos en que fantasmas virtuales los atacaban desde la oscuridad =)
Escalada
De repente, recibimos un pedido de un gran juego de disparos de realidad virtual para 8 jugadores con armas en la mano. Y estos son 16 objetos que hay que hacer temblar. Fue una suerte que el escenario asumiera la posibilidad de dividir el rastreo en dos zonas de 4 jugadores cada una, así que decidí que no habría problemas, podías tomar el pedido y no preocuparte por nada. Fue imposible probar el sistema en casa. requería un área grande y una gran cantidad de equipo que sería comprado por el cliente, así que antes de la instalación decidí dedicar tiempo a automatizar la calibración de seguimiento.
Autocalibración
Fue increíblemente inconveniente dirigir las cámaras, colgar todas estas pegatinas, medir manualmente las coordenadas. Quería deshacerme de todos estos procesos: colgar las cámaras de la excavadora, caminar aleatoriamente con el marcador en el espacio y ejecutar el algoritmo de calibración. En teoría, esto debería ser posible, pero no está claro cómo abordar la escritura de un algoritmo.
El primer paso fue centralizar todo el sistema. En lugar de dividir el área de juego en bloques de 8 cámaras, hice un solo servidor, que recibía las coordenadas de los puntos en los marcos de todas las cámaras a la vez.
La idea es la siguiente:
- Cuelgo cámaras y las dirijo a la zona de juego a ojo
- Comienzo el modo de grabación en el servidor, en el que todos los puntos 2D procedentes de las cámaras se guardan en un archivo
- Camino por un lugar oscuro del juego con un marcador en mis manos
- Dejo de grabar y comienzo el cálculo de los datos de calibración, que calcula las ubicaciones, orientaciones y distancias focales de todas las cámaras.
- como resultado del párrafo anterior, se obtiene un único espacio lleno de cámaras. Porque este espacio no está ligado a coordenadas reales, tiene un desplazamiento y rotación aleatorios, que resto manualmente.
Tuve que hojear una gran cantidad de material sobre álgebra lineal y escribir cientos de líneas de código Python. Tanto es así que apenas recuerdo cómo funciona.
Así es como se ve una varilla de calibración especial impresa en una impresora.
Probando un gran proyecto
Los problemas comenzaron durante las pruebas en las instalaciones un par de semanas antes del lanzamiento del proyecto. La identificación de 8 colores de marcadores diferentes funcionó terriblemente, los jugadores de prueba se teletransportaron constantemente entre sí, algunos colores no diferían en absoluto de los reflejos externos en el centro comercial. Los vanos intentos de arreglar algo con cada noche de insomnio me llevaron cada vez más a la desesperación. Todo esto se vio agravado por la falta de rendimiento del servidor al calcular decenas de miles de líneas rectas por segundo.
Cuando el nivel de cortisol en sangre superó el máximo teórico, decidí mirar el problema desde un ángulo diferente. ¿Cómo se puede reducir el número de puntos de colores sin reducir el número de marcadores? Active el seguimiento. Deje que cada jugador, por ejemplo, siempre tenga un cuerno rojo a la izquierda. Y el segundo a veces se enciende en verde cuando llega un comando del servidor, de modo que en un momento solo lo enciende un jugador. Resulta que la luz verde parecerá saltar de un jugador a otro, actualizando el enlace de seguimiento a la luz roja y restableciendo el error de orientación del magnetómetro.
Para hacer esto, tuve que correr al chipidip más cercano, comprar LED, cables, transistores, un soldador, cinta aislante y colgar la funcionalidad de control LED en el tablero del traje, que no fue diseñado para esto, en mocos. Es bueno que al conectar la placa, por si acaso, colgué un par de patas stm-ki libres en las almohadillas de contacto.
Los algoritmos de seguimiento tuvieron que ser considerablemente complicados, ¡pero al final funcionó! La teletransportación de los jugadores entre sí desapareció, la carga en el procesador cayó, las bengalas dejaron de interferir.
El proyecto se lanzó con éxito, primero hice nuevos tableros de trajes con soporte para rastreo activo, e hicimos una actualización de hardware.
¿Cómo terminó?
Durante los últimos 3 años, hemos abierto muchos lugares de entretenimiento en todo el mundo, pero el coronavirus ha hecho sus propios ajustes, lo que nos dio la oportunidad de cambiar la dirección del trabajo en una dirección socialmente más útil. Ahora tenemos bastante éxito en el desarrollo de simuladores médicos en realidad virtual. Nuestro equipo es todavía pequeño y estamos buscando activamente expandir nuestro personal. Si hay desarrolladores experimentados de UE4 que buscan trabajo entre los lectores de Habr, por favor escríbeme.
Momento divertido tradicional al final del artículo:
Periódicamente, durante las pruebas con una gran cantidad de jugadores, surgió un problema técnico en el que el jugador fue teletransportado repentinamente durante un corto tiempo a una altura de varios metros, lo que provocó la reacción correspondiente. Resultó que el modelo de mi cámara asumió la intersección de la matriz con una línea infinita que partía del marcador. Pero ella no tuvo en cuenta que la cámara tiene un frente y un respaldo, por lo que el sistema buscó la intersección de líneas sin fin, incluso si el punto está detrás de la cámara. Por lo tanto, hubo situaciones en las que dos cámaras diferentes vieron dos marcadores diferentes, pero el sistema pensó que era un marcador a una altura de varios metros.
El sistema funcionaba literalmente por el culo.