Exibimos conteúdo na imagem reconhecida de acordo com certas regras

Às vezes, quando você lê uma tarefa técnica e define prazos para implementação, subestime a quantidade de tempo e esforço gastos na solução de um problema específico. Acontece que um ponto, calculado pelo tempo por semana, é realizado em uma hora e, às vezes, vice-versa. Mas este artigo não é sobre isso. Esta é uma demonstração da evolução de uma solução para um problema. Desde o seu início até a implementação.



Termos Utilizados


  • Marca ou marcador - uma imagem carregada no mecanismo AR, reconhecida pela câmera do dispositivo (tablet ou smartphone) e pode ser identificada exclusivamente


  • Encontrado - estado do marcador quando foi detectado no campo de visão da câmera


  • Perdido - estado do marcador quando foi perdido da visualização da câmera


  • Ele pode ser exibido - quando o marcador é encontrado, exibimos o conteúdo anexado ao marcador


  • Não pode ser exibido - quando encontramos o marcador, não exibimos o conteúdo - Conteúdo anexado ao marcador - qualquer objeto (modelo 3D, sprite, sistema de partículas etc.) que pode ser anexado ao marcador e que, consequentemente, será exibido na tela se um marcador for encontrado


  • Marca, marcador, encontrado, perdido - os estados básicos inerentes a todos os mecanismos, oferecendo funcionalidade de reconhecimento


  • Pode ser exibido e não pode ser exibido - o estado usado para resolver este problema


    Um exemplo:


    • Faça o download do aplicativo => todas as marcas baixadas são reconhecíveis
    • Estamos tentando reconhecer => o estado do marcador muda para "encontrado"
    • Se o marcador puder ser exibido => indique que o marcador foi “encontrado” e exibimos o modelo anexado ao marcador
    • Se o marcador não puder ser exibido => o estado do marcador é “encontrado”, mas o modelo anexado não é exibido
    • A marca desapareceu do campo de visão da câmera => mudamos o estado para "perdido"


1. Introdução


Há um grande cartão postal do tamanho de uma folha A4. É dividido em 4 partes iguais (formato de uma parte A5), em cada uma dessas partes existe:


  • Uma marca de canto completa (1)
  • Metade da marca do lado inferior (5)
  • Metade da marca do lado superior (8)
  • Marca de um quarto de centro (9)

imagem


Se você trabalhou com algum mecanismo de reconhecimento, por exemplo, o Vuforia, provavelmente sabe que não existe "qualidade de reconhecimento". A marca é reconhecida ou não. Portanto, se o mecanismo "vê" a marca, ele altera o estado para Find e o método OnSuccess() é OnSuccess() ; se "o perdeu", o estado muda para Lost e o método OnLost() é OnLost() . Dessa forma, a partir das condições existentes e dos dados de entrada, surgiu uma situação ao ter uma parte do cartão (metade ou um quarto) possível reconhecer a marca.


O fato é que, de acordo com a tarefa técnica, um desbloqueio gradual dos personagens foi planejado. Nessa situação, é possível um desbloqueio gradual, mas, como não há pessoas que tentam reconhecer um quarto ou metade da marca.


Declaração de tarefa


É necessário implementar a lógica na forma de código do programa, o que garante o desbloqueio gradual do conteúdo anexado aos marcadores. A partir da localização dos elementos no cartão, é sabido que os marcadores 1, 2, 3, 4 estão disponíveis para exibição inicialmente.


imagem


Se o conteúdo foi lido e exibido em 2 marcadores, por exemplo, 2 e 3, permitimos exibir o conteúdo no marcador 6. Se o marcador 1 ainda não foi lido, o acesso ao marcador 5 é fechado. Mais por analogia. Nós meio que permitimos exibir o conteúdo em marcadores laterais somente quando lemos marcadores de canto adjacentes.


imagem


Se marcadores de 1 a 8 estiverem disponíveis e forem encontrados, abra o conteúdo no marcador 9. Para exibição, cada marcador possui 2 estados - o conteúdo está disponível e não está disponível para a exibição, pelo qual o campo public bool IsActive; é responsável public bool IsActive;


Fica imediatamente claro que essa deve ser uma máquina de estados com uma transição entre estados ou uma implementação do padrão "Estado".


Spoiler

O resultado não foi um, nem outro. Não posso dizer que isso é uma muleta porque a solução atendeu totalmente aos requisitos no começo do artigo. Mas você pode discutir comigo.


Com isso, dou a oportunidade de pensar um pouco sobre possíveis soluções e implementações dessa tarefa. Levei cerca de 5 horas para perceber e fixar na minha cabeça a imagem da decisão.


Para maior clareza, gravei um vídeo no qual o resultado final do algoritmo (se é que você pode chamar assim) já está capturado.



Abordagens de solução


1. Dos marcadores de canto ao centro


A primeira coisa que veio à mente foi apresentar as interações entre os marcadores do canto ao centro. Em forma gráfica, fica assim:


imagem


Os problemas:


  1. Como determinar qual rótulo lateral deve mudar de estado? O da esquerda ou da direita? Também forçamos cada marcador a "saber" sobre a existência de um marcador central.
  2. É necessário adicionar dependências não óbvias da categoria: o marcador lateral assina o evento de marcador de canto IsChangedEventCallback (), ações semelhantes devem ser executadas para o marcador central.
  3. Se considerarmos cada tipo de marcador como uma entidade, na hierarquia dessas entidades encaminharemos o comando de mudança de estado de baixo para cima. Isso não é muito bom, porque nos vinculamos firmemente ao número, neste caso, marcadores angulares, perdendo a capacidade de escalar.

Incapaz de colocar a solução acima em minha mente por causa dos muitos casos extremos e da complexidade da percepção, mudei a abordagem para escolher o marcador no qual as dependências começam a se espalhar.


2. As laterais sabem sobre central e canto


Pensando na solução do parágrafo 3 da abordagem anterior, surgiu a idéia de mudar o tipo de marcador, a partir do qual os estados de outros marcadores começam a mudar. Como os principais marcadores laterais foram tomados. Nesse cenário, as comunicações (dependências) ficam assim:


imagem


A partir daqui, fica imediatamente claro que as conexões da lateral para a central são supérfluas, porque o marcador lateral não precisa saber nada sobre o marcador central, portanto essa abordagem foi imediatamente transformada na final.


3. O central sabe de todos, os laterais sabem da esquina


imagem


A solução final é quando o marcador lateral conhece os cantos, os cantos “vivem a vida” e o central sabe o estado de todos os marcadores.


imagem


Trabalhar com a exibição de cartão postal não é muito conveniente. Os relacionamentos entre entidades não parecem claros o suficiente para converter isso facilmente em código. Uma tentativa de interpretar na forma de uma árvore binária pode introduzir alguma ambiguidade. Mas aqui uma das propriedades da árvore binária é violada, portanto a ambiguidade desaparece imediatamente. A partir do qual podemos concluir que essa representação pode ser inequivocamente interpretada e usada para representar graficamente a solução para o problema. Com base nessas conclusões, usaremos notação gráfica, a saber:


  • Marcador de ângulo - Nó de ângulo (nível 3)
  • Marcador lateral - Nó lateral (nível 2)
  • Marcador central - Nó central (nível 1)

Vantagens:


  1. As dependências entre os marcadores são óbvias e óbvias.
  2. Cada um dos níveis pode ser representado na forma de 3 entidades, cada uma das quais consiste em partes básicas, mas com suas adições inerentes a cada um dos níveis.
  3. Para expandir, você só precisará adicionar um novo tipo de nó com suas próprias características
  4. É fácil imaginar esta solução em um estilo OO (orientado a objetos)

Implementação


Entidades base


Vamos criar uma interface que contenha os elementos inerentes a cada entidade (nome, estado):


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

A seguir, descrevemos a essência de cada nó:


  • CornerNode - um nó angular. Basta implementar a interface INode :

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

Por que IsActive = true ?


A resposta

A partir das condições do problema, o conteúdo dos marcadores de canto está inicialmente disponível para reconhecimento.


  • SideNode - um nó lateral. Implementamos a interface LeftCornerNode , mas adicionamos os RightCornerNode e RightCornerNode . Assim, o nó lateral mantém seu estado em si e sabe apenas sobre a existência de nós laterais.

 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 é o nó central. Como nos anteriores, implementamos o INode . Adicione um campo do 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; } } 

Classe Opencard


Métodos e campos privados


Agora que criamos todos os elementos do cartão que criamos (todos os tipos de marcadores), podemos começar a descrever a essência do próprio cartão. Não estou acostumado a iniciar uma classe com um construtor. Eu sempre começo com os métodos básicos que são inerentes a uma entidade específica. Vamos começar com campos particulares e métodos particulares.


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

Com campos, tudo é bem simples. 2 listas com nós laterais angulares e um campo do nó central.


Em seguida, você precisa esclarecer um pouco. O fato é que o próprio marcador é do tipo Trackable e não tem idéia (e não deveria ter) de que faz parte de outra lógica lá. Portanto, tudo o que podemos usar para controlar a exibição é o nome dele. Portanto, se o marcador em si não armazena o tipo de nó ao qual ele pertence, devemos transferir essa responsabilidade para a nossa classe OpenCard . Com base nisso, primeiro descrevemos três métodos particulares responsáveis ​​por determinar o tipo de nó.


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

Mas esses métodos não fazem sentido para usar diretamente. Não é conveniente operar com valores booleanos quando você trabalha com objetos de outro nível de abstração. Portanto, criaremos um enum NodeType simples enum NodeType e um método privado GetNodeType() , que encapsula em si toda a lógica associada à determinação do tipo de nó.


 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 é um método que retorna um valor booleano indicando se nossa marca pertence a um cartão postal. Este é um método auxiliar, feito para que, se o marcador não pertencer a nenhum cartão, possamos exibir o conteúdo nele.

 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 - um método (como o nome indica) no qual verificamos o estado atual do nó e alteramos seu 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; } } 

Construtor


Quando todas as cartas estiverem sobre a mesa, podemos finalmente ir ao construtor. Pode haver várias abordagens para inicialização. Mas eu decidi livrar a classe OpenCard de gestos desnecessários, tanto quanto possível. Deve responder conosco se o conteúdo está disponível para exibição ou não. Portanto, simplesmente pedimos listas de entrada de nós de 2 tipos e um nó 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); } 

Observe que, como o nó central precisa apenas verificar a condição de todos os outros nós true , basta INode implicitamente INode nós central e angular que entraram no construtor no tipo INode .


Inicialização


Qual é a maneira mais conveniente de criar objetos que não precisam ser anexados (como componentes MonoBehaviour ) a um GameObject? - Certo, ScriptableObject . Além disso, por conveniência, adicione o atributo MenuItem , que simplificará a criação de novos cartões.


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

O acorde final em nossa composição será uma passagem pela matriz de ScriptableObject adicionados (se houver) e a criação de cartões postais a partir deles. Depois disso, resta a nós, no método Update , verificar simplesmente se podemos exibir o conteúdo ou não.


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

Conclusões


Para mim, pessoalmente, as conclusões foram as seguintes:


  1. Ao tentar resolver um problema, você precisa quebrar seus elementos em partes atômicas. Além disso, considerando todas as opções possíveis para a interação entre essas partes atômicas, você precisa começar com o objeto, do qual mais conexões potencialmente virão. De outra maneira, ele pode ser formulado da seguinte forma: esforce-se para começar a resolver problemas com elementos que, potencialmente, serão menos confiáveis
  2. Se possível, você deve tentar apresentar os dados de origem em um formato diferente. No meu caso, a representação gráfica me ajudou muito.
  3. Cada entidade é separada da outra pelo número de conexões que poderiam vir dela.
  4. Muitas tarefas aplicadas que são mais habituais de resolver escrevendo um algoritmo podem ser representadas no estilo OO
  5. Uma solução que possui dependências em anel é uma solução ruim
  6. Se é difícil manter todas as conexões entre objetos em sua cabeça, esta é uma má decisão
  7. Se você não consegue ter em mente a lógica da interação dos objetos - esta é uma má decisão
  8. Suas muletas nem sempre são uma má decisão

Você conhece outra solução? - Escreva nos comentários.

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


All Articles