我们根据某些规则在识别的图像上显示内容

有时,当您阅读技术任务并设定实施期限时,会低估解决特定问题所花费的时间和精力。 碰巧一个点(按每周的时间估算)在一个小时完成,有时反之亦然。 但是这篇文章不是关于这个的。 这是对问题解决方案演变的演示。 从成立到实施。



使用条款


  • 标记或标记-上传到AR引擎的图像,可由设备的相机(平板电脑或智能手机)识别,并且可以唯一地标识


  • 找到-在相机视野中检测到的标记状态


  • 丢失-从相机视图丢失时的标记状态


  • 它可以显示-找到标记后,我们将显示标记所附加的内容


  • 无法显示-找到标记时,不显示内容-标记所附加的内容-可以附加到标记上并因此将被显示的任何对象(3D模型,子画面,粒子系统等)在屏幕上是否找到标记


  • 标记,标记,发现,丢失-提供识别功能的所有引擎固有的基本状态


  • 它可以显示而不能显示-用于解决此问题的状态


    一个例子:


    • 下载应用程序=>可以识别所有下载的品牌
    • 我们正在尝试识别=>标记的状态更改为“找到”
    • 如果标记可以显示=>状态,则标记为“找到”,我们将显示附加到标记的模型
    • 如果无法显示标记=>标记的状态为“找到”,但不显示附加的模型
    • 品牌从相机的视野中消失了=>我们将状态更改为“丢失”


引言


有一张A4大小的大明信片。 它分为4个相等的部分(A5格式的一部分),在每个这些部分上有:


  • 1个完整的角标记(1)
  • 底边标记的一半(5)
  • 顶部标记的一半(8)
  • 四分之一中心马克(9)

图片


如果您使用任何识别引擎(例如Vuforia),那么您可能知道不存在“识别质量”之类的东西。 标记被识别或未被识别。 因此,如果引擎“看到”品牌,则将状态更改为FindOnSuccess()方法;如果引擎“丢失”,状态将更改为LostOnLost()方法。 因此,根据现有条件和输入数据,当拥有一部分卡(一半或四分之一)时出现了一种情况,便有可能识别该品牌。


事实是,根据技术任务,计划逐步解锁角色。 在这种情况下,可以逐步解锁,但是鉴于没有人试图识别该品牌的四分之一或一半。


任务说明


有必要以程序代码的形式实现逻辑,以确保逐渐释放附加在标记上的内容。 从卡上元素的位置可以知道,标记1、2、3、4最初可用于显示。


图片


如果已读取内容并将其显示在2个标记(例如2和3)上,则我们允许在标记6上显示内容。如果尚未读取标记1,则将关闭对标记5的访问。 进一步类推。 我们仅在读取相邻的角标记时才允许在侧标记处显示内容。


图片


如果找到并找到了1到8个标记,则打开标记9上的内容以进行显示,每个标记具有2个状态-内容可用,不可用于显示,由public bool IsActive;字段负责public bool IsActive;


马上很清楚,这应该是在状态之间进行转换的状态机,或者是“状态​​”模式的实现。


扰流板

结果不是一个,不是另一个。 我不能说这是一个拐杖,因为该解决方案完全满足了本文开头的要求。 但是你可以和我争论。


在此基础上,我让您有机会自己考虑一下此任务的可能解决方案和实现。 我花了大约5个小时才意识到并确定了决定的图片。


为了清楚起见,我录制了一段视频,上面已经捕获了算法的最终结果(如果可以调用它的话)。



解决方法


1.从角标记到中心


首先想到的是呈现从角落到中心的标记之间的相互作用。 在图形形式中,它看起来像这样:


图片


问题:


  1. 如何确定要更改状态的哪一侧标签? 左边还是右边的那个? 我们还强迫每个标记“知道”中心标记的存在。
  2. 有必要从类别中添加非显而易见的依赖项:侧面标记订阅了IsChangedEventCallback()角标记事件,必须对中央标记执行类似的操作。
  3. 如果我们将每种类型的标记都视为一个实体,则在这些实体的层次结构中,我们将从下至上转发状态更改命令。 这不是很好,因为我们将数字与角度标记紧密绑定在一起,在这种情况下,角度标记失去了缩放的能力。

由于存在许多极端情况和感知的复杂性,无法将上述解决方案付诸实践,我改变了选择依赖项开始扩散的标记的方法。


2.侧面知道中心和角落


考虑到前一种方法的第3段的解决方案,想法是改变标记的类型,其他标记的状态也从此开始改变。 作为主要的侧面标记。 在这种情况下,通信(依赖项)如下所示:


图片


从这里可以立即清楚地看到,从横向到中心的连接是多余的,因为横向标记不需要了解任何有关中心标记的信息,因此该方法立即转变为最终方法。


3.中央的人知道每个人,侧面的人知道角落


图片


最终的解决方案是,当侧面标记知道拐角时,这些拐角“过自己的生活”,而中心标记知道所有标记的状态。


图片


使用明信片视图不是很方便。 实体之间的关系看起来不够清晰,无法轻松地将其转换为代码。 尝试以二叉树的形式进行解释可能会带来一些歧义。 但是这里二叉树的属性之一被破坏了,因此歧义立即消失了。 从中我们可以得出结论,可以清楚地解释此表示形式,并以图形方式表示问题的解决方案。 基于这些结论,我们将使用图形符号,即:


  • 角度标记-角度节点(级别3)
  • 侧面标记-侧面节点(级别2)
  • 中心标记-中心节点(级别1)

优点:


  1. 标记之间的依赖性是显而易见的。
  2. 每个级别可以以3个实体的形式表示,每个实体都由基本部分组成,但是每个级别都具有其固有的附加功能
  3. 要扩展,您只需要添加具有自己特征的新型节点
  4. 这种解决方案很容易以OO(面向对象)风格来想象

实作


基础实体


让我们创建一个包含每个实体(名称,状态)固有元素的接口:


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

接下来,我们描述每个节点的本质:


  • CornerNode-一个角度节点。 只需实现INode接口:

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

为什么IsActive = true


答案

根据问题的情况,角标记的内容最初可用于识别。


  • SideNode-侧节点。 我们实现了INode接口,但添加了LeftCornerNodeRightCornerNode 。 因此,侧节点保持其自身状态并且仅知道侧节点的存在。

 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是中央节点。 和前面的一样,我们实现了INode 。 添加类型为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; } } 

开卡类


私有方法和字段


现在,我们已经创建了所制作的卡片的所有元素(各种标记),我们可以开始描述卡片本身的本质。 我不习惯用构造函数开始类。 我总是从特定实体固有的基本方法开始。 让我们从私有字段和私有方法开始。


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

使用字段,一切都非常简单。 2个列表,其中包含角节点,侧节点和中心节点的一个字段。


接下来,您需要澄清一下。 事实是标记本身是可Trackable的类型,并且不知道(也不应该)它是那里其他逻辑的一部分。 因此,我们可以用来控制显示的全部就是他的名字。 因此,如果标记本身没有存储它所属的节点的类型,那么我们必须将此责任转移给我们的OpenCard类。 基于此,我们首先描述三种负责确定节点类型的私有方法。


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

但是,直接使用这些方法没有意义。 当您使用另一个抽象级别的对象时,使用布尔值进行操作并不方便。 因此,我们将创建一个简单的enum NodeType和一个私有方法GetNodeType() ,该方法本身封装了与确定节点类型相关的所有逻辑。


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

公开方法


  • IsExist是一种返回布尔值的方法,该值指示我们的品牌是否属于明信片。 这是一种辅助方法,可以这样做,以便如果标记不属于任何卡,我们就可以在其上显示内容。

 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一种方法(顾名思义),其中我们检查节点的当前状态并更改其状态。

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

建设者


当所有卡片都放在桌子上时,我们终于可以去构造函数了。 初始化可以有几种方法。 但是我决定尽可能多地摆脱OpenCard类不必要的手势。 它应该向我们回答内容是否可以显示。 因此,我们只要求输入2种类型的节点和一个中心节点的输入列表。


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

请注意,由于中央节点仅需要检查所有其他true节点的条件,对于我们而言, INode构造函数中INode成角度的中央节点隐式INodeINode类型就足够了。


初始化


创建不需要附加到GameObject的对象(例如MonoBehaviour组件)的最便捷方法是什么? -对, ScriptableObject 。 另外,为方便起见,添加MenuItem属性,该属性将简化新卡的创建。


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

我们创作的最后一个和弦将是通过添加的(如果有) ScriptableObject数组以及从中创建明信片的过程。 在那之后,我们只需在Update方法中检查是否可以显示内容即可。


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

结论


对我个人而言,结论如下:


  1. 在尝试解决问题时,您需要尝试将其元素分解为原子部分。 此外,考虑这些原子部分之间相互作用的所有可能选择,您需要从可能会产生更多连接的对象开始。 换句话说,它可以表述为:努力开始使用可能不太可靠的元素来解决问题
  2. 如果可能,您应尝试以其他形式显示源数据。 就我而言,图形表示对我有很大帮助。
  3. 每个实体彼此之间的间隔可能来自于该实体之间的连接数。
  4. 可以通过OO样式表示许多更习惯于通过编写算法来解决的应用任务
  5. 具有环依赖性的解决方案是一个糟糕的解决方案
  6. 如果很难将所有物体之间的连接保持在脑海中,这是一个错误的决定
  7. 如果您不能记住对象交互的逻辑,这是一个错误的决定
  8. 拐杖并不总是一个错误的决定

您知道其他解决方案吗? -在评论中写。

Source: https://habr.com/ru/post/zh-CN454452/


All Articles