Mostramos contenido en la imagen reconocida de acuerdo con ciertas reglas

A veces, cuando lee una tarea técnica y establece plazos para la implementación, subestima la cantidad de tiempo y esfuerzo dedicado a resolver un problema en particular. Sucede que un punto, que se estima por tiempo por semana, se lleva a cabo a una hora, y a veces viceversa. Pero este artículo no se trata de eso. Esta es una demostración de la evolución de una solución a un problema. Desde su inicio hasta su implementación.



Términos usados


  • Marca o marcador: una imagen cargada en el motor AR, que es reconocida por la cámara del dispositivo (tableta o teléfono inteligente) y puede identificarse de manera única


  • Encontrado: estado del marcador cuando se detectó en el campo de visión de la cámara


  • Perdido: estado del marcador cuando se perdió de la vista de la cámara


  • Se puede mostrar: cuando se encuentra el marcador, mostramos el contenido adjunto al marcador


  • No se puede mostrar - cuando encontramos el marcador, no mostrar el contenido - Contenido adjunto al marcador - ningún objeto (modelo 3D, sprite, sistema de partículas, etc.) que se puede adjuntar al marcador y que, en consecuencia, se mostrará en la pantalla si se encuentra un marcador


  • Marca, marcador, encontrado, perdido: los estados básicos inherentes a todos los motores que proporcionan funcionalidad de reconocimiento


  • Se puede mostrar y no se puede mostrar: el estado utilizado para resolver este problema


    Un ejemplo:


    • Descargue la aplicación => todas las marcas descargadas son reconocibles
    • Estamos tratando de reconocer => el estado del marcador cambia a "encontrado"
    • Si el marcador puede mostrarse => indica que el marcador está "encontrado" y mostramos el modelo adjunto al marcador
    • Si el marcador no se puede mostrar => el estado del marcador es "encontrado", pero el modelo adjunto no se muestra
    • La marca desapareció del campo de visión de la cámara => cambiamos el estado a "perdido"


Introduccion


Hay una postal grande del tamaño de una hoja A4. Se divide en 4 partes iguales (formato de una parte A5), en cada una de estas partes hay:


  • Una marca de esquina completa (1)
  • La mitad de la marca lateral inferior (5)
  • La mitad de la marca lateral superior (8)
  • Cuarto centro marca (9)

imagen


Si trabajó con algún motor de reconocimiento, por ejemplo, Vuforia, entonces probablemente sepa que no existe la "calidad de reconocimiento". La marca se reconoce o no se reconoce. En consecuencia, si el motor "ve" la marca, cambia el estado a Find y se OnSuccess() método OnSuccess() , si se "pierde", el estado cambia a Lost y se OnLost() método OnLost() . En consecuencia, a partir de las condiciones existentes y los datos de entrada, surgió una situación en la que al tener una parte de la tarjeta (medio o cuarto) era posible reconocer la marca.


La cuestión es que, según la tarea técnica, se planificó un desbloqueo gradual de los personajes. En esta situación, es posible un desbloqueo gradual, pero dado que no hay personas que intenten reconocer una cuarta parte o la mitad de la marca.


Declaración de tarea


Es necesario implementar la lógica en forma de código de programa, que asegura el desbloqueo gradual del contenido adjunto a los marcadores. Desde la ubicación de los elementos en la tarjeta se sabe que los marcadores 1, 2, 3, 4 están disponibles para mostrar inicialmente.


imagen


Si el contenido se ha leído y mostrado en 2 marcadores, por ejemplo, 2 y 3, entonces permitimos mostrar el contenido en el marcador 6. Si el marcador 1 aún no se ha leído, entonces se cierra el acceso al marcador 5. Además por analogía. De algún modo, damos permiso para mostrar contenido en los marcadores laterales solo cuando hemos leído los marcadores de esquina adyacentes.


imagen


Si los marcadores del 1 al 8 están disponibles y se encuentran, abra el contenido en el marcador 9. Para visualización, cada marcador tiene 2 estados: el contenido está disponible y no está disponible para la visualización, de lo cual es responsable el campo public bool IsActive;


Está claro de inmediato que esto debería ser una máquina de estados con una transición entre estados o una implementación del patrón "Estado".


Spoiler

El resultado no fue uno, ni otro. No puedo decir que esto sea una muleta porque la solución cumplió completamente con los requisitos al principio del artículo. Pero puedes discutir conmigo.


En esto, le doy la oportunidad de pensar un poco sobre las posibles soluciones e implementaciones de esta tarea. Me llevó unas 5 horas darme cuenta y fijar en mi cabeza la imagen de la decisión.


Para mayor claridad, grabé un video en el que el resultado final del algoritmo (si se puede llamar así) ya está capturado.



Enfoques de solución


1. De los marcadores de esquina al centro


Lo primero que se me ocurrió fue presentar las interacciones entre los marcadores desde la esquina hasta el centro. En forma gráfica, se ve así:


imagen


Los problemas:


  1. ¿Cómo determinar qué etiqueta lateral cambiar de estado? ¿El de la izquierda o la derecha? También forzamos a cada marcador a "saber" acerca de la existencia de uno central.
  2. Es necesario agregar dependencias no obvias de la categoría: el marcador lateral se suscribe al evento de marcador de esquina IsChangedEventCallback (), se deben realizar acciones similares para el marcador central.
  3. Si consideramos cada tipo de marcador como una entidad, en la jerarquía de estas entidades reenviaremos el comando de cambio de estado de abajo hacia arriba. Esto no es muy bueno, porque nos unimos estrechamente con el número, en este caso, los marcadores angulares, perdiendo la capacidad de escalar.

Incapaz de poner en mi cabeza la solución anterior debido a los muchos casos extremos y la complejidad de la percepción, cambié el enfoque para elegir el marcador en el que las dependencias comienzan a extenderse.


2. Los laterales saben sobre el centro y la esquina.


Pensando en la solución del párrafo 3 del enfoque anterior, surgió la idea de cambiar el tipo de marcador, a partir del cual los estados de otros marcadores comienzan a cambiar. Como se tomaron los marcadores laterales principales. En este escenario, las comunicaciones (dependencias) se ven así:


imagen


A partir de aquí, queda claro de inmediato que las conexiones desde el lateral al central son superfluas, porque el marcador lateral no necesita saber nada sobre el marcador central, por lo tanto, este enfoque se transformó inmediatamente en el último.


3. El central sabe de todos, los secundarios saben de la esquina.


imagen


La solución final es cuando el marcador lateral sabe acerca de las esquinas, las esquinas "viven su vida", y el marcador central sabe sobre el estado de todos los marcadores.


imagen


Trabajar con la vista de postal no es muy conveniente. Las relaciones entre entidades no se ven lo suficientemente claras como para convertir esto fácilmente en código. Un intento de interpretar en forma de árbol binario puede introducir cierta ambigüedad. Pero aquí se viola una de las propiedades del árbol binario, por lo que la ambigüedad desaparece de inmediato. De lo cual podemos concluir que esta representación puede ser interpretada sin ambigüedades y utilizada para representar gráficamente la solución al problema. En base a estas conclusiones, utilizaremos la notación gráfica, a saber:


  • Angle Marker - Angle Node (nivel 3)
  • Marcador lateral - Nodo lateral (nivel 2)
  • Marcador central - Nodo central (nivel 1)

Ventajas:


  1. Las dependencias entre los marcadores son obvias y obvias.
  2. Cada uno de los niveles se puede representar en forma de 3 entidades, cada una de las cuales consta de partes básicas, pero con sus adiciones inherentes a cada uno de los niveles.
  3. Para expandir, solo necesitará agregar un nuevo tipo de nodo con sus propias características
  4. Esta solución es fácil de imaginar en un estilo OO (orientado a objetos)

Implementación


Entidades base


Creemos una interfaz que contenga los elementos inherentes a cada entidad (nombre, estado):


 public interface INode { string Name { get; set; } bool IsActive { get; set; } } 

A continuación, describimos la esencia de cada nodo:


  • CornerNode : un nodo angular. Simplemente implemente la interfaz INode :

 public class CornerNode : INode { public string Name { get; set; } public bool IsActive { get; set; } public Node(string name) { Name = name; IsActive = true; } } 

¿Por qué IsActive = true ?


La respuesta

A partir de las condiciones del problema, el contenido de los marcadores de esquina está inicialmente disponible para su reconocimiento.


  • SideNode : un nodo lateral. Implementamos la interfaz INode , pero agregamos los RightCornerNode LeftCornerNode y RightCornerNode . Por lo tanto, el nodo lateral mantiene su estado en sí mismo y solo conoce la existencia de nodos laterales.

 public class SideNode : INode { public string Name { get; set; } public bool IsActive { get; set; } public CornerNode LeftCornerNode { get; } public CornerNode RightCornerNode { get; } public SideNode(string name, CornerNode leftNode, CornerNode rightNode) { Name = name; IsActive = false; LeftCornerNode = leftNode; RightCornerNode = rightNode; } } 

  • CenterNode es el nodo central. Como en los anteriores, implementamos INode . Agregue un campo de tipo List<INode> .

 public class CentralNode : INode { public List<INode> NodesOnCard; public string Name { get; set; } public bool IsActive { get; set; } public CentralNode(string name) { Name = name; IsActive = false; } } 

Clase Opencard


Métodos y campos privados.


Ahora que hemos creado todos los elementos de la tarjeta que hemos creado (todo tipo de marcadores), podemos comenzar a describir la esencia de la tarjeta en sí. No estoy acostumbrado a comenzar una clase con un constructor. Siempre empiezo con los métodos básicos que son inherentes a una entidad en particular. Comencemos con campos privados y métodos privados.


 private List<CornerNode> cornerNodes; private List<SideNode> sideNodes; private CentralNode centralNode; 

Con los campos, todo es bastante simple. 2 listas con nodos angulares, laterales y un campo del nodo central.


A continuación, necesita aclarar un poco. El hecho es que el marcador en sí es del tipo Trackable y no tiene idea (y no debería tener) de que es parte de alguna otra lógica allí. Por lo tanto, todo lo que podemos usar para controlar la pantalla es su nombre. En consecuencia, si el marcador en sí no almacena el tipo de nodo al que pertenece, entonces debemos transferir esta responsabilidad a nuestra clase OpenCard . En base a esto, primero describimos 3 métodos privados que son responsables de determinar el tipo de nodo.


 private bool IsCentralNode(string name) { return name == centralNode.Name; } private bool IsSideNode(string name) { foreach (var sideNode in sideNodes) if (sideNode.Name == name) return true; return false; } private bool IsCornerNode(string name) { foreach (var sideNode in cornerNodes) if (sideNode.Name == name) return true; return false; } 

Pero estos métodos no tienen sentido para usar directamente. No es conveniente operar con valores booleanos cuando trabaja con objetos de otro nivel de abstracción. Por lo tanto, crearemos un enum NodeType simple y un método privado GetNodeType() , que encapsula en sí mismo toda la lógica asociada con la determinación del tipo de nodo.


 public enum NodeType { CornerNode, SideNode, CentralNode } private NodeType? GetNodeType(string name) { if (IsCentralNode(name)) return NodeType.CentralNode; if (IsSideNode(name)) return NodeType.SideNode; if (IsCornerNode(name)) return NodeType.CornerNode; return null; } 

Métodos públicos


  • IsExist es un método que devuelve un valor booleano que indica si nuestra marca pertenece a una postal. Este es un método auxiliar, que se realiza para que si el marcador no pertenece a ninguna tarjeta, podamos mostrar el contenido en él.

 public bool IsExist(string name) { foreach (var node in centralNode.NodesOnCard) if (node.Name == name) return true; if (centralNode.Name == name) return true; return false; } 

  • CheckOnActiveAndChangeStatus : un método (como su nombre lo indica) en el que verificamos el estado actual del nodo y cambiamos su estado.

 public bool CheckOnActiveAndChangeStatus(string name) { switch (GetNodeType(name)) { case NodeType.CornerNode: foreach (var node in cornerNodes) if (node.Name == name) return node.IsActive = true; return false; case NodeType.SideNode: foreach (var node in sideNodes) if (node.LeftCornerNode.IsActive && node.RightCornerNode.IsActive) return true; return false; case NodeType.CentralNode: foreach (var node in centralNode.NodesOnCard) if (!node.IsActive) return false; return centralNode.IsActive = true; default: return false; } } 

Constructor


Cuando todas las cartas están sobre la mesa, finalmente podemos ir al constructor. Puede haber varios enfoques para la inicialización. Pero decidí librar a la clase OpenCard de gestos innecesarios tanto como sea posible. Debería responder con nosotros si el contenido está disponible para mostrar o no. Por lo tanto, simplemente pedimos listas de entrada de nodos de 2 tipos y un nodo central.


 public OpenCard(List<CornerNode> listCornerNode, List<SideNode> listSideNode, CentralNode centralNode) { CornerNodes = listCornerNode; SideNodes = listSideNode; CentralNodes = centralNode; CentralNodes.NodesOnCard = new List<INode>(); foreach (var node in CornerNodes) CentralNodes.NodesOnCard.Add(node); foreach (var node in SideNodes) CentralNodes.NodesOnCard.Add(node); } 

Tenga en cuenta que dado que el nodo central solo necesita verificar la condición de todos los demás nodos true , es suficiente para nosotros INode implícitamente INode nodos angulados y centrales que entraron en el constructor al tipo INode .


Inicialización


¿Cuál es la forma más conveniente de crear objetos que no necesitan estar unidos (como los componentes MonoBehaviour ) a un GameObject? - Correcto, ScriptableObject . Además, para mayor comodidad, agregue el atributo MenuItem , que simplificará la creación de nuevas tarjetas.


 [CreateAssetMenu(fileName = "Open Card", menuName = "New Open Card", order = 51)] public class OpenCardScriptableObject : ScriptableObject { public string leftDownName; public string rightDownName; public string rightUpName; public string leftUpName; public string leftSideName; public string rightSideName; public string downSideName; public string upSideName; public string centralName; } 

El acorde final en nuestra composición será un pasaje a través del conjunto de ScriptableObject agregados (si los hay) y la creación de postales a partir de ellos. Después de eso, nos queda en el método Update simplemente verificar si podemos mostrar el contenido o no.


 public OpenCardScriptableObject[] openCards; private List<OpenCard> _cardList; void Awake() { if (openCards.Length != 0) { _cardList = new List<OpenCard>(); foreach (var card in openCards) { var leftDown = new CornerNode(card.leftDownName); var rightDown = new CornerNode(card.rightDownName); var rightUp = new CornerNode(card.rightUpName); var leftUp = new CornerNode(card.leftUpName); var leftSide = new SideNode(card.leftSideName, leftUp, leftDown); var downSide = new SideNode(card.downSideName, leftDown, rightDown); var rightSide = new SideNode(card.rightSideName, rightDown, rightUp); var upSide = new SideNode(card.upSideName, rightUp, leftUp); var central = new CentralNode(card.centralName); var nodes = new List<CornerNode>() {leftDown, rightDown, rightUp, leftUp}; var sideNodes = new List<SideNode>() {leftSide, downSide, rightSide, upSide}; _cardList.Add(new OpenCard(nodes, sideNodes, central)); } } } void Update() { var isNotPartCard = false; foreach (var card in _cardList) { if (card.IsExist(trackableName)) isNotPartCard = true; if (card.CheckOnActiveAndChangeStatus(trackableName)) imageTrackablesMap[trackableName].OnTrackSuccess(trackable); if (!isNotPartCard) imageTrackablesMap[trackableName].OnTrackSuccess(trackable); } } 

Conclusiones


Para mí personalmente, las conclusiones fueron las siguientes:


  1. Cuando intente resolver un problema, debe intentar dividir sus elementos en partes atómicas. Además, teniendo en cuenta todas las opciones posibles para la interacción entre estas partes atómicas, debe comenzar con el objeto, del que potencialmente surgirán más conexiones. De otra manera, puede formularse como: esforzarse por comenzar a resolver problemas con elementos que, potencialmente, serán menos confiables
  2. Si es posible, debe intentar presentar los datos de origen en una forma diferente. En mi caso, la representación gráfica me ayudó mucho.
  3. Cada entidad está separada de la otra por el número de conexiones que potencialmente podrían provenir de ella.
  4. Muchas tareas aplicadas que son más habituales de resolver escribiendo un algoritmo pueden representarse en el estilo OO
  5. Una solución que tiene dependencias de anillo es una mala solución
  6. Si es difícil mantener todas las conexiones entre los objetos en tu cabeza, esta es una mala decisión
  7. Si no puede tener en cuenta la lógica de la interacción de los objetos, esta es una mala decisión
  8. Tus muletas no siempre son una mala decisión

¿Conoces otra solución? - Escribe en los comentarios.

Source: https://habr.com/ru/post/454452/


All Articles