Wir zeigen Inhalte auf dem erkannten Bild nach bestimmten Regeln an

Wenn Sie eine technische Aufgabe lesen und Fristen für die Implementierung festlegen, unterschätzen Sie manchmal den Zeit- und Arbeitsaufwand für die Lösung eines bestimmten Problems. Es kommt vor, dass ein Punkt, der durch die Zeit pro Woche geschätzt wird, nach einer Stunde ausgeführt wird und manchmal auch umgekehrt. Aber in diesem Artikel geht es nicht darum. Dies ist eine Demonstration der Entwicklung einer Lösung für ein Problem. Von der Gründung bis zur Umsetzung.



Verwendete Begriffe


  • Markierung oder Markierung - Ein in die AR-Engine geladenes Bild, das von der Kamera des Geräts (Tablet oder Smartphone) erkannt wird und eindeutig identifiziert werden kann


  • Gefunden - Markierungsstatus, als er im Sichtfeld der Kamera erkannt wurde


  • Lost - Markierungsstatus, als er aus der Kameraansicht verloren ging


  • Es kann angezeigt werden - wenn der Marker gefunden wird, zeigen wir den an den Marker angehängten Inhalt an


  • Kann nicht angezeigt werden - wenn wir den Marker finden, zeigen Sie den Inhalt nicht an - Inhalt, der an den Marker angehängt ist - jedes Objekt (3D-Modell, Sprite, Partikelsystem usw.), das an den Marker angehängt werden kann und dementsprechend angezeigt wird auf dem Bildschirm, wenn eine Markierung gefunden wird


  • Markieren, markieren, gefunden, verloren - die Grundzustände aller Engines, die Erkennungsfunktionen bieten


  • Es kann angezeigt werden und kann nicht angezeigt werden - der Status, der zur Lösung dieses Problems verwendet wird


    Ein Beispiel:


    • Anwendung herunterladen => Alle heruntergeladenen Marken sind erkennbar
    • Wir versuchen zu erkennen => der Zustand des Markers ändert sich in "gefunden"
    • Wenn der Marker angezeigt werden kann => Geben Sie an, dass der Marker „gefunden“ wurde, und wir zeigen das an den Marker angehängte Modell an
    • Wenn der Marker nicht angezeigt werden kann => wird der Status des Markers "gefunden", aber das angehängte Modell wird nicht angezeigt
    • Die Marke ist aus dem Sichtfeld der Kamera verschwunden => Wir ändern den Status in "verloren".


Einführung


Es gibt eine große Postkarte in der Größe eines A4-Blattes. Es ist in 4 gleiche Teile unterteilt (Format eines Teils A5). Auf jedem dieser Teile befindet sich:


  • Eine volle Eckmarke (1)
  • Eine Hälfte der unteren Seitenmarkierung (5)
  • Eine Hälfte der oberen Seitenmarkierung (8)
  • Quarter Center Mark (9)

Bild


Wenn Sie mit Erkennungs-Engines wie Vuforia gearbeitet haben, wissen Sie wahrscheinlich, dass es keine „Erkennungsqualität“ gibt. Die Marke wird entweder erkannt oder nicht erkannt. Wenn die Engine die Marke "sieht", ändert sie den Status in " Find und die OnSuccess() -Methode wird OnSuccess() . Wenn sie "verloren" wird, ändert sich der Status in " Lost und die OnLost() -Methode wird OnLost() . Dementsprechend ergab sich aus den vorhandenen Bedingungen und Eingabedaten eine Situation, in der es möglich war, mit einem Teil der Karte (einem halben oder einem Viertel) die Marke zu erkennen.


Die Sache ist, dass entsprechend der technischen Aufgabe eine schrittweise Freischaltung der Charaktere geplant war. In dieser Situation ist ein schrittweises Entsperren möglich, da jedoch keine Personen versuchen, ein Viertel oder die Hälfte der Marke zu erkennen.


Aufgabenstellung


Es ist notwendig, Logik in Form von Programmcode zu implementieren, der das schrittweise Entsperren des an die Markierungen angehängten Inhalts gewährleistet. Aus der Position der Elemente auf der Karte ist bekannt, dass die Markierungen 1, 2, 3, 4 anfänglich zur Anzeige verfügbar sind.


Bild


Wenn der Inhalt gelesen und auf 2 Markern angezeigt wurde, z. B. 2 und 3, können Sie den Inhalt auf Marker 6 anzeigen. Wenn Marker 1 noch nicht gelesen wurde, ist der Zugriff auf Marker 5 geschlossen. Weiter analog. Wir geben die Erlaubnis, Inhalte nur an Seitenmarkierungen anzuzeigen, wenn wir benachbarte Eckmarkierungen gelesen haben.


Bild


Wenn Marker von 1 bis 8 verfügbar sind und gefunden werden, öffnen Sie den Inhalt auf Marker 9 zur Anzeige. Jeder Marker hat 2 Zustände - der Inhalt ist verfügbar und nicht für die Anzeige verfügbar, für die das Feld public bool IsActive; des public bool IsActive; verantwortlich ist public bool IsActive;


Es ist sofort klar, dass dies entweder eine Zustandsmaschine mit einem Übergang zwischen Zuständen oder eine Implementierung des "Zustands" -Musters sein sollte.


Spoiler

Das Ergebnis war nicht das eine, nicht das andere. Ich kann nicht sagen, dass dies eine Krücke ist, da die Lösung die Anforderungen am Anfang des Artikels vollständig erfüllt. Aber du kannst mit mir streiten.


In diesem Zusammenhang gebe ich Ihnen die Möglichkeit, selbst über mögliche Lösungen und Implementierungen dieser Aufgabe nachzudenken. Ich brauchte ungefähr 5 Stunden, um das Bild der Entscheidung zu erkennen und in meinem Kopf zu fixieren.


Aus Gründen der Übersichtlichkeit habe ich ein Video aufgenommen, auf dem das Endergebnis des Algorithmus (wenn Sie es so nennen können) bereits erfasst ist.



Lösungsansätze


1. Von den Eckmarkierungen zur Mitte


Das erste, was mir in den Sinn kam, war die Darstellung der Wechselwirkungen zwischen den Markern von der Ecke bis zur Mitte. In grafischer Form sieht es so aus:


Bild


Die Probleme:


  1. Wie bestimme ich, welches Seitenetikett den Status ändern soll? Der links oder rechts? Wir zwingen jeden Marker auch dazu, über die Existenz eines zentralen Markers „Bescheid zu wissen“.
  2. Es ist erforderlich, nicht offensichtliche Abhängigkeiten aus der Kategorie hinzuzufügen: Die Seitenmarkierung abonniert das Eckmarkierungsereignis IsChangedEventCallback (). Für die zentrale Markierung müssen ähnliche Aktionen ausgeführt werden.
  3. Wenn wir jeden Markertyp als Entität betrachten, leiten wir in der Hierarchie dieser Entitäten den Befehl zum Ändern des Status von unten nach oben weiter. Dies ist nicht sehr gut, da wir uns eng an die Zahl, in diesem Fall an Winkelmarkierungen, binden und die Skalierbarkeit verlieren.

Ich konnte die obige Lösung aufgrund der vielen Randfälle und der Komplexität der Wahrnehmung nicht in meinen Kopf setzen und änderte den Ansatz zur Auswahl eines Markers, von dem sich die Abhängigkeiten zu verbreiten beginnen.


2. Seitenteile kennen sich mit Mittel und Ecke aus


Beim Nachdenken über die Lösung von Absatz 3 des vorherigen Ansatzes kam die Idee, den Markertyp zu ändern, ab dem sich die Zustände anderer Marker zu ändern beginnen. Als Hauptseitenmarkierungen wurden genommen. In diesem Szenario sieht die Kommunikation (Abhängigkeiten) folgendermaßen aus:


Bild


Ab hier wird sofort klar, dass die Verbindungen von der lateralen zur zentralen überflüssig sind, da der laterale Marker nichts über den zentralen Marker wissen muss, weshalb dieser Ansatz sofort in den endgültigen umgewandelt wurde.


3. Der zentrale kennt jeden, die seitlichen kennen die Ecke


Bild


Die endgültige Lösung ist, wenn der Seitenmarker über die Ecken Bescheid weiß, die Ecken „ihr Leben leben“ und der zentrale über den Zustand aller Markierungen Bescheid weiß.


Bild


Das Arbeiten mit der Postkartenansicht ist nicht sehr bequem. Die Beziehungen zwischen Entitäten sehen nicht klar genug aus, um dies einfach in Code umzuwandeln. Ein Versuch, in Form eines Binärbaums zu interpretieren, kann zu Mehrdeutigkeiten führen. Hier wird jedoch eine der Eigenschaften des Binärbaums verletzt, sodass die Mehrdeutigkeit sofort verschwindet. Daraus können wir schließen, dass diese Darstellung eindeutig interpretiert und verwendet werden kann, um die Lösung des Problems grafisch darzustellen. Basierend auf diesen Schlussfolgerungen werden wir die Graphnotation verwenden, nämlich:


  • Winkelmarkierung - Winkelknoten (Stufe 3)
  • Seitenmarkierung - Seitenknoten (Stufe 2)
  • Center Marker - Center Node (Stufe 1)

Vorteile:


  1. Die Abhängigkeiten zwischen den Markern sind offensichtlich und offensichtlich.
  2. Jede der Ebenen kann in Form von 3 Entitäten dargestellt werden, von denen jede aus grundlegenden Teilen besteht, deren Ergänzungen jedoch jeder der Ebenen inhärent sind
  3. Zum Erweitern müssen Sie nur einen neuen Knotentyp mit eigenen Merkmalen hinzufügen
  4. Diese Lösung ist in einem OO-Stil (objektorientiert) leicht vorstellbar

Implementierung


Basisentitäten


Erstellen wir eine Schnittstelle, die die Elemente enthält, die jeder Entität inhärent sind (Name, Status):


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

Als nächstes beschreiben wir die Essenz jedes Knotens:


  • CornerNode - ein Winkelknoten . Implementieren INode einfach die INode Schnittstelle:

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

Warum ist IsActive = true ?


Die Antwort

Unter den Bedingungen des Problems steht der Inhalt von Eckmarkierungen zunächst zur Erkennung zur Verfügung.


  • SideNode - ein Seitenknoten . Wir implementieren die INode Schnittstelle, fügen jedoch die RightCornerNode LeftCornerNode und RightCornerNode . Somit behält der Seitenknoten seinen Zustand in sich und weiß nur über die Existenz von Seitenknoten Bescheid.

 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 ist der zentrale Knoten. Wie in den vorherigen implementieren wir INode . Fügen Sie ein Feld vom Typ 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; } } 

Opencard-Klasse


Private Methoden und Felder


Nachdem wir alle Elemente der Karte erstellt haben (alle Arten von Markierungen), können wir beginnen, die Essenz der Karte selbst zu beschreiben. Ich bin es nicht gewohnt, eine Klasse mit einem Konstruktor zu beginnen. Ich beginne immer mit den grundlegenden Methoden, die einer bestimmten Entität inhärent sind. Beginnen wir mit privaten Feldern und privaten Methoden.


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

Mit Feldern ist alles ganz einfach. 2 Listen mit Winkel-, Seitenknoten und einem Feld des Zentralknotens.


Als nächstes müssen Sie ein wenig klären. Tatsache ist, dass der Marker selbst vom Typ Trackable und keine Ahnung hat (und nicht haben sollte), dass er Teil einer anderen Logik ist. Daher können wir nur seinen Namen verwenden, um die Anzeige zu steuern. Wenn der Marker selbst nicht den Knotentyp speichert, zu dem er gehört, müssen wir diese Verantwortung auf unsere OpenCard Klasse übertragen. Basierend darauf beschreiben wir zunächst 3 private Methoden, die für die Bestimmung des Knotentyps verantwortlich sind.


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

Die direkte Verwendung dieser Methoden ist jedoch nicht sinnvoll. Es ist nicht bequem, mit Booleschen Werten zu arbeiten, wenn Sie mit Objekten einer anderen Abstraktionsebene arbeiten. Daher werden wir eine einfache enum NodeType und eine private Methode GetNodeType() , die die gesamte Logik zur Bestimmung des Knotentyps in sich zusammenfasst.


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

Öffentliche Methoden


  • IsExist ist eine Methode, die einen booleschen Wert zurückgibt, der angibt, ob unsere Marke zu einer Postkarte gehört. Dies ist eine Hilfsmethode, die durchgeführt wird, damit wir den Inhalt darauf anzeigen können, wenn der Marker zu keiner Karte gehört.

 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 - eine Methode (wie der Name schon sagt), mit der wir den aktuellen Status des Knotens überprüfen und seinen Status ändern.

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

Konstruktor


Wenn alle Karten auf dem Tisch liegen, können wir endlich zum Konstruktor gehen. Es gibt verschiedene Ansätze zur Initialisierung. Aber ich habe beschlossen, die OpenCard Klasse so weit wie möglich von unnötigen Gesten zu befreien. Es sollte uns antworten, ob der Inhalt zur Anzeige verfügbar ist oder nicht. Daher fragen wir einfach nach Eingabelisten von Knoten von 2 Typen und einem zentralen Knoten.


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

Da der zentrale Knoten nur die Bedingung überprüfen muss, dass alle anderen true Knoten vorhanden sind, reicht es für uns aus INode abgewinkelten und zentralen Knoten, die in den Konstruktor INode sind, implizit in den Typ INode zu INode .


Initialisierung


Was ist der bequemste Weg, um Objekte zu erstellen, die nicht an ein GameObject angehängt werden müssen (wie MonoBehaviour Komponenten)? - Richtig, ScriptableObject . MenuItem Sie der MenuItem auch das MenuItem Attribut hinzu, um die Erstellung neuer Karten zu vereinfachen.


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

Der letzte Akkord in unserer Komposition wird eine Passage durch eine Reihe von hinzugefügten (falls vorhanden) ScriptableObject und daraus Postkarten erstellen. Danach bleibt es für uns in der Update Methode, einfach zu prüfen, ob wir den Inhalt anzeigen können oder nicht.


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

Schlussfolgerungen


Für mich persönlich waren die Schlussfolgerungen wie folgt:


  1. Wenn Sie versuchen, ein Problem zu lösen, müssen Sie versuchen, seine Elemente in atomare Teile zu zerlegen. Wenn Sie alle möglichen Optionen für die Interaktion zwischen diesen atomaren Teilen berücksichtigen, müssen Sie mit dem Objekt beginnen, von dem möglicherweise weitere Verbindungen stammen. Auf andere Weise kann es wie folgt formuliert werden: Bemühen Sie sich, Probleme mit Elementen zu lösen, die möglicherweise weniger zuverlässig sind
  2. Wenn möglich, sollten Sie versuchen, die Quelldaten in einer anderen Form darzustellen. In meinem Fall hat mir die grafische Darstellung sehr geholfen.
  3. Jede Entität ist durch die Anzahl der Verbindungen, die möglicherweise von ihr stammen könnten, von der anderen getrennt.
  4. Viele angewandte Aufgaben, deren Lösung durch Schreiben eines Algorithmus üblicher ist, können im OO-Stil dargestellt werden
  5. Eine Lösung mit Ringabhängigkeiten ist eine schlechte Lösung
  6. Wenn es schwierig ist, alle Verbindungen zwischen Objekten in Ihrem Kopf aufrechtzuerhalten, ist dies eine schlechte Entscheidung
  7. Wenn Sie die Logik der Interaktion von Objekten nicht berücksichtigen können, ist dies eine schlechte Entscheidung
  8. Ihre Krücken sind nicht immer eine schlechte Entscheidung

Kennen Sie eine andere Lösung? - Schreiben Sie in die Kommentare.

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


All Articles