Nous affichons le contenu sur l'image reconnue selon certaines règles

Parfois, lorsque vous lisez une tâche technique et définissez des délais de mise en œuvre, sous-estimez le temps et les efforts consacrés à la résolution d'un problème particulier. Il arrive qu'un point, qui est estimé par le temps par semaine, soit réalisé à une heure, et parfois vice versa. Mais cet article ne traite pas de cela. Il s'agit d'une démonstration de l'évolution d'une solution à un problème. De sa création à sa mise en œuvre.



Termes utilisés


  • Marque ou marqueur - une image chargée dans le moteur AR, qui est reconnue par l'appareil photo de l'appareil (tablette ou smartphone) et qui peut être identifiée de manière unique


  • Trouvé - état du marqueur lorsqu'il a été détecté dans le champ de vision de la caméra


  • Lost - état du marqueur lorsqu'il a été perdu de la vue de la caméra


  • Il peut être affiché - lorsque le marqueur est trouvé, nous affichons le contenu attaché au marqueur


  • Ne peut pas être affiché - lorsque nous trouvons le marqueur, n'affichez pas le contenu - Contenu attaché au marqueur - tout objet (modèle 3D, sprite, système de particules, etc.) qui peut être attaché au marqueur et qui, en conséquence, sera affiché à l'écran si un marqueur est trouvé


  • Marque, marqueur, trouvé, perdu - les états de base inhérents à tous les moteurs offrant une fonctionnalité de reconnaissance


  • Il peut être affiché et ne peut pas être affiché - l'état utilisé pour résoudre ce problème


    Un exemple:


    • Téléchargez l'application => toutes les marques téléchargées sont reconnaissables
    • Nous essayons de reconnaître => l'état du marqueur passe à "trouvé"
    • Si le marqueur peut être affiché => indiquer que le marqueur est «trouvé» et nous affichons le modèle attaché au marqueur
    • Si le marqueur ne peut pas être affiché => l'état du marqueur est «trouvé», mais le modèle attaché n'est pas affiché
    • La marque a disparu du champ de vision de la caméra => on change l'état en "perdu"


Présentation


Il y a une grande carte postale de la taille d'une feuille A4. Il est divisé en 4 parties égales (format d'une partie A5), sur chacune de ces parties il y a:


  • Une marque de coin complète (1)
  • La moitié de la marque latérale inférieure (5)
  • La moitié de la marque latérale supérieure (8)
  • Quart de centre (9)

image


Si vous avez travaillé avec des moteurs de reconnaissance, par exemple, Vuforia, vous savez probablement que la «qualité de reconnaissance» n'existe pas. La marque est reconnue ou non reconnue. Par conséquent, si le moteur «voit» la marque, il change l'état en Find et la méthode OnSuccess() est OnSuccess() , s'il la «perd», l'état change en Lost et la méthode OnLost() est OnLost() . En conséquence, à partir des conditions existantes et des données saisies, une situation s'est produite lorsque la possession d'une partie de la carte (la moitié ou le quart) a permis de reconnaître la marque.


Le fait est que selon la tâche technique, un déverrouillage progressif des personnages était prévu. Dans cette situation, un déverrouillage progressif est possible, mais étant donné qu'il n'y a personne qui essaie de reconnaître un quart ou la moitié de la marque.


Énoncé de tâche


Il est nécessaire de mettre en œuvre une logique sous forme de code de programme, ce qui assure le déverrouillage progressif du contenu attaché aux marqueurs. De l'emplacement des éléments sur la carte, il est connu que les marqueurs 1, 2, 3, 4 sont disponibles pour l'affichage initial.


image


Si le contenu a été lu et affiché sur 2 marqueurs, par exemple 2 et 3, alors nous autorisons l'affichage du contenu sur le marqueur 6. Si le marqueur 1 n'a pas encore été lu, alors l'accès au marqueur 5 est fermé. Plus loin par analogie. Nous autorisons en quelque sorte l'affichage du contenu sur les marqueurs latéraux uniquement lorsque nous avons lu les marqueurs de coin adjacents.


image


Si des marqueurs de 1 à 8 sont disponibles et trouvés, ouvrez le contenu du marqueur 9 pour l'affichage. Chaque marqueur a 2 états - le contenu est disponible et non disponible pour l'affichage, dont le champ public bool IsActive; est responsable public bool IsActive;


Il est immédiatement clair que cela devrait être soit une machine à états avec une transition entre les États, soit une mise en œuvre du modèle «État».


Spoiler

Le résultat n'était pas un, pas un autre. Je ne peux pas dire que c'est une béquille car la solution répondait pleinement aux exigences du début de l'article. Mais vous pouvez discuter avec moi.


Sur ce point, je vous donne l'opportunité de réfléchir un peu aux solutions et implémentations possibles de cette tâche. Il m'a fallu environ 5 heures pour réaliser et fixer dans ma tête l'image de la décision.


Pour plus de clarté, j'ai enregistré une vidéo sur laquelle le résultat final de l'algorithme (si vous pouvez l'appeler ainsi) est déjà capturé.



Approches de solution


1. Des marqueurs de coin au centre


La première chose qui m'est venue à l'esprit était de présenter les interactions entre les marqueurs du coin au centre. Sous forme graphique, cela ressemble à ceci:


image


Les problèmes:


  1. Comment déterminer quelle étiquette latérale changer d'état? Celui de gauche ou de droite? Nous forçons également chaque marqueur à «connaître» l'existence d'un marqueur central.
  2. Il est nécessaire d'ajouter des dépendances non évidentes de la catégorie: le marqueur latéral souscrit à l'événement de marqueur de coin IsChangedEventCallback (), des actions similaires doivent être effectuées pour le marqueur central.
  3. Si nous considérons chaque type de marqueur comme une entité, alors dans la hiérarchie de ces entités, nous transmettrons la commande de changement d'état de bas en haut. Ce n'est pas très bon, car nous nous lions étroitement avec le nombre, dans ce cas, les marqueurs angulaires, perdant la capacité de mise à l'échelle.

Incapable de mettre la solution ci-dessus dans ma tête en raison des nombreux cas marginaux et de la complexité de la perception, j'ai changé l'approche pour choisir un marqueur sur lequel les dépendances commencent à se propager.


2. Les latéraux connaissent le centre et l'angle


En réfléchissant à la solution du paragraphe 3 de l'approche précédente, l'idée est venue de changer le type de marqueur, à partir duquel les états des autres marqueurs commencent à changer. Comme les principaux marqueurs latéraux ont été pris. Dans ce scénario, les communications (dépendances) ressemblent à ceci:


image


De là, il devient immédiatement clair que les connexions du latéral au central sont superflues, car le marqueur latéral n'a besoin de rien savoir du marqueur central, donc cette approche a été immédiatement transformée en finale.


3. Le central connaît tout le monde, les côtés connaissent le coin


image


La solution finale est lorsque le marqueur latéral connaît les coins, que les coins «vivent leur vie» et que le central connaît l'état de tous les marqueurs.


image


Travailler avec la vue de carte postale n'est pas très pratique. Les relations entre les entités ne semblent pas assez claires pour les convertir facilement en code. Une tentative d'interprétation sous la forme d'un arbre binaire peut introduire une certaine ambiguïté. Mais ici, l'une des propriétés de l'arbre binaire est violée, donc l'ambiguïté disparaît immédiatement. D'où nous pouvons conclure que cette représentation peut être interprétée sans ambiguïté et utilisée pour représenter graphiquement la solution du problème. Sur la base de ces conclusions, nous utiliserons la notation graphique, à savoir:


  • Angle Marker - Angle Node (niveau 3)
  • Marqueur latéral - Noeud latéral (niveau 2)
  • Marqueur central - Noeud central (niveau 1)

Avantages:


  1. Les dépendances entre les marqueurs sont évidentes et évidentes.
  2. Chacun des niveaux peut être représenté sous la forme de 3 entités, chacune composée de parties de base, mais avec leurs ajouts inhérents à chacun des niveaux
  3. Pour vous développer, vous n'aurez qu'à ajouter un nouveau type de nœud avec ses propres caractéristiques
  4. Cette solution est facile à imaginer dans un style OO (orienté objet)

Implémentation


Entités de base


Créons une interface qui contient les éléments inhérents à chaque entité (nom, état):


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

Ensuite, nous décrivons l'essence de chaque nœud:


  • CornerNode - un nœud angulaire. INode interface INode :

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

Pourquoi IsActive = true ?


La réponse

D'après les conditions du problème, le contenu des marqueurs de coin est initialement disponible pour la reconnaissance.


  • SideNode - un nœud latéral. Nous implémentons l'interface INode , mais ajoutons les RightCornerNode LeftCornerNode et RightCornerNode . Ainsi, le nœud latéral conserve son état en lui-même et ne connaît que l'existence de nœuds latéraux.

 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 est le nœud central. Comme dans les précédents, nous implémentons INode . Ajoutez un champ de type 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; } } 

Classe Opencard


Méthodes et champs privés


Maintenant que nous avons créé tous les éléments de la carte que nous avons créés (toutes sortes de marqueurs), nous pouvons commencer à décrire l'essence de la carte elle-même. Je n'ai pas l'habitude de démarrer une classe avec un constructeur. Je commence toujours par les méthodes de base inhérentes à une entité particulière. Commençons par les champs privés et les méthodes privées.


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

Avec les champs, tout est assez simple. 2 listes avec nœuds latéraux angulaires et un champ du nœud central.


Ensuite, vous devez clarifier un peu. Le fait est que le marqueur lui-même est de type Trackable et il n'a aucune idée (et ne devrait pas avoir) qu'il fait partie d'une autre logique là-bas. Par conséquent, tout ce que nous pouvons utiliser pour contrôler l'affichage est son nom. Par conséquent, si le marqueur lui-même ne stocke pas le type de nœud auquel il appartient, nous devons transférer cette responsabilité à notre classe OpenCard . Sur cette base, nous décrivons d'abord 3 méthodes privées chargées de déterminer le type de nœud.


 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; } 

Mais ces méthodes n'ont pas de sens à utiliser directement. Il n'est pas pratique d'utiliser des valeurs booléennes lorsque vous travaillez avec des objets d'un autre niveau d'abstraction. Par conséquent, nous allons créer une enum NodeType simple enum NodeType et une méthode privée GetNodeType() , qui encapsule en elle-même toute la logique associée à la détermination du type de nœud.


 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éthodes publiques


  • IsExist est une méthode qui renvoie une valeur booléenne indiquant si notre marque appartient à une carte postale. Il s'agit d'une méthode auxiliaire, qui est effectuée de sorte que si le marqueur n'appartient à aucune carte, nous pouvons afficher le contenu dessus.

 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 - une méthode (comme son nom l'indique) dans laquelle nous vérifions l'état actuel du nœud et changeons son état.

 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; } } 

Constructeur


Lorsque toutes les cartes sont sur la table, nous pouvons enfin aller chez le constructeur. Il peut y avoir plusieurs approches à l'initialisation. Mais j'ai décidé de débarrasser autant que possible la classe OpenCard des gestes inutiles. Il devrait répondre avec nous si le contenu est disponible pour l'affichage ou non. Par conséquent, nous demandons simplement des listes d'entrée de nœuds de 2 types et un nœud 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); } 

Notez que puisque le nœud central n'a besoin que de vérifier la condition que tous les autres nœuds true , il nous suffit de INode implicitement INode nœuds angulaires et centraux entrés dans le constructeur en type INode .


Initialisation


Quelle est la façon la plus pratique de créer des objets qui n'ont pas besoin d'être attachés (comme les composants MonoBehaviour ) à un GameObject? - D' ScriptableObject , ScriptableObject . De plus, pour plus de commodité, ajoutez l'attribut MenuItem , qui simplifiera la création de nouvelles cartes.


 [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; } 

L'accord final dans notre composition sera un passage à travers le tableau des ScriptableObject ajoutés (le cas échéant) et la création de cartes postales à partir d'eux. Après cela, il nous reste dans la méthode de Update à Update de vérifier simplement si nous pouvons afficher le contenu ou non.


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

Conclusions


Pour moi personnellement, les conclusions étaient les suivantes:


  1. Lorsque vous essayez de résoudre un problème, vous devez essayer de diviser ses éléments en parties atomiques. En outre, compte tenu de toutes les options possibles pour l'interaction entre ces parties atomiques, vous devez commencer par l'objet, d'où proviendront potentiellement plus de connexions. D'une autre manière, il peut être formulé comme suit: s'efforcer de commencer à résoudre des problèmes avec des éléments qui, potentiellement, seront moins fiables
  2. Si possible, vous devez essayer de présenter les données source sous une forme différente. Dans mon cas, la représentation graphique m'a beaucoup aidé.
  3. Chaque entité est séparée de l'autre par le nombre de connexions qui pourraient en découler.
  4. De nombreuses tâches appliquées qui sont plus habituelles à résoudre en écrivant un algorithme peuvent être représentées dans le style OO
  5. Une solution qui a des dépendances en anneau est une mauvaise solution
  6. S'il est difficile de garder toutes les connexions entre les objets dans votre tête, c'est une mauvaise décision
  7. Si vous ne pouvez pas garder à l'esprit la logique de l'interaction des objets - c'est une mauvaise décision
  8. Vos béquilles ne sont pas toujours une mauvaise décision

Connaissez-vous une autre solution? - Écrivez dans les commentaires.

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


All Articles