ARKit教程

ARKit教程14_第十章:检测占位符

2019-08-20  本文已影响16人  张芳涛

前言

ARKit感兴趣的同学,可以订阅ARKit教程专题
源代码地址在这里

正文

本章重点介绍如何检测占位符并首先显示一些位置标记,以及之后的平面。

平面检测与物体检测

不要将平面检测与物体检测混淆;他们是两个不同的东西。平面检测内置于ARKit中,以帮助程序员将对象放入场景中。

ARKit的平面检测不是RazeAd所需的工具。相反,我们将使用Vision Framework来检测有限形状,然后我们将ARKit平面基于该形状。

检测矩形

iOS的一个更好的方面是不同框架之间的互操作性。在这种情况下,我们正在利用Vision框架。视觉框架是一种图像分析和计算机视觉框架,用于识别和分类现实世界的对象。

我们将使用Vision框架检测矩形,然后使用VNDetectRectanglesRequest将该矩形转换为ARKit对象,顾名思义,它检测矩形形状。

Vision静态检测对象,因此我们必须为每个请求提供图像。
现在,我们可以使用屏幕上的点按来触发图像分析。

ARKit导入后再导入Vision框架:

import Vision

我们添加以下代码:

// 1 
guard let currentFrame = sceneView.session.currentFrame else { 
    return
}

// 2 
DispatchQueue.global(qos: .background).async {

    // 3 
    do { 
        // 4 
        let request = VNDetectRectanglesRequest {(request, error) in

             // Access the first result in the array, 
             // after converting to an array 
             // of VNRectangleObservation 
             // 5 
            guard let results = request.results?.compactMap({ $0 as? VNRectangleObservation }),
                 // 6
                let result = results.first else {
                print ("[Vision] VNRequest produced no result")
                return 
                  }
            } 
        } catch(let error) { 
    print( "An error occurred during rectangle detection: \(error)") 
    }
}  

上面的代码作用如下:

请求将返回最多一个值,因为VNDetectRectanglesRequestmaximumObservations属性默认为1

let result = results.first else ... block之后,添加以下代码:

// 1 
let coordinates: [matrix_float4x4] = [ 
    result.topLeft, 
    result.topRight, 
    result.bottomRight, 
    result.bottomLeft ].compactMap { 
    // 2 
    guard let hitFeature = currentFrame.hitTest( $0, types: .featurePoint).first else { return nil }
    // 3 
    return hitFeature.worldTransform
}
// 4 
guard coordinates.count == 4 else { return }
// 5 
DispatchQueue.main.async {
    // 6
    self.removeBillboard()
    let (topLeft, topRight, bottomRight, bottomLeft) = (coordinates[0], coordinates[1], coordinates[2], coordinates[3])
// 7 
self.createBillboard(topLeft: topLeft, topRight: topRight, bottomRight: bottomRight, bottomLeft: bottomLeft)
}

上面的代码作用如下:

注意:Vision适用于2D图像,因此它不了解3D世界。结果点始终是位图图像中的2D坐标。

  我们将这四个坐标转换为一个地图友好的数组,以便更容易按顺序处理它们。

currentFrame.hitTest(_:types :)将一个点投射到一个3D对象。 types参数确定它们是什么类型的对象,包括:

为了检测表面上的矩形,我们可能认为最后三个中的任何一个都可能是一个不错的选择。事实上,ARKit中的平面有一个对齐。由于我们希望检测任何表面上的矩形 - 因此不限于水平或垂直 - 只留下一个选项:featurePoint

还有一个缺失部分需要完成touchesBegan()实现:使用VNDetectRectanglesRequest实例。

catch回调之前,我们添加下面的代码:

// 1 
let handler = VNImageRequestHandler( cvPixelBuffer: currentFrame.capturedImage)

// 2 
try handler.perform([request])

注意如何为单帧定义处理程序实例,但它可用于在同一帧上执行多个请求,例如文本识别,条形码检测等。如果需要执行多个分析,则创建一个请求每个分析,但只有一个处理程序。

创建广告牌

现在我们有四个矩阵确定四个矩形顶点中每个矩形顶点的位置和方向。我们将使用这些来帮助定位广告牌。

世界变换矩阵包含通过从相机向相反方向投影2D点及其方向而得到的交点。方向取决于从摄像机到矩形顶点的虚线之间的角度,如下图中的红线所示,矩形方向由垂直于平面的直线确定,如绿线所示。

现在我们需要添加一个扩展:

func createBillboard(
topLeft: matrix_float4x4, topRight: matrix_float4x4, bottomRight: matrix_float4x4, bottomLeft: matrix_float4x4) {
    // 1 
    let plane = RectangularPlane( topLeft: topLeft, topRight: topRight, bottomLeft: bottomLeft, bottomRight: bottomRight)
    // 2
    let anchor = ARAnchor(transform: plane.center)
    // 3 
    billboard = BillboardContainer(billboardAnchor: anchor, plane: plane)
    // 4 
    sceneView.session.add(anchor: anchor)
    print("New billboard created")
}

上面的代码作用如下:

BillboardContainer是一个实用程序数据结构,用于存储有关在BillboardContainer.swift中实现的广告牌的数据。现在我们将扩展BillboardContainer

我们需要添加的最后一段代码是删除广告牌。在createBillboard()之后立即添加以下函数:

func removeBillboard() {
    // 1 
    if let anchor = billboard?.billboardAnchor {
    // 2 
    sceneView.session.remove(anchor: anchor) 
    // 3 
    billboard?.billboardNode?.removeFromParentNode() billboard = nil 
    }
}

上面代码作用如下:

下面的物体都是可以检测到的矩形形状:

重要的是它的颜色必须与它背后的表面形成鲜明对比,所以白色桌子上的白纸有可能识别不出来。

Xcode启动应用程序后,将iPhone的相机指向我们选择检测的对象并点按屏幕。

如果你没有可以扫描的对象,那就对着屏幕扫描下面这个黑色的正方形吧:

如果你可以扫描到,那么控制台会有如下提示:

如果Vision无法检测到形状,我们将在控制台中看到如下消息:

[Vision] VNRequest produced no result

检测到之后会有如下提示:

New billboard created

显示地标

我们可能希望在检测点时显示占位符。这可以帮助我们直观地调试应用程序。

我们在ViewController.swift文件中的touchesBegan()方法中在self.createBillboard调用之后加入如下代码:

for coordinate in coordinates {
    // 1 
    let box = SCNBox(width: 0.01, height: 0.01, length: 0.001, chamferRadius: 0.0)
    // 2
   let node = SCNNode(geometry: box)
    // 3 
    node.transform = SCNMatrix4(coordinate)
    // 4 
    self.sceneView.scene.rootNode.addChildNode(node)
}

构建并运行并尝试检测矩形形状;你会看到类似的东西:

注意每个矩形顶点的小白色矩形。当完成视觉测试时,可以注释掉该代码。

注意:我们还可以选择不同的形状,大小,颜色,方向以及可能需要的任何内容,以使每个地标在拥挤的场景中脱颖而出。

添加SceneKit节点

createBillboard中,我们创建了一个ARKit锚点并将其添加到ARKit会话中。下一步是将ARKit锚转换为SceneKit节点。

接下来我们需要完善ARSCNViewDelegate方法:

func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {

    // 1 
    guard let billboard = billboard else { return nil } 
    var node: SCNNode? = nil

    // 2 
    //DispatchQueue.main.sync { switch anchor { 
        // 3 
        case billboard.billboardAnchor:
            let billboardNode = addBillboardNode() 
            node = billboardNode 
        default:
             break 
            } 
        //}
   return node
}

当手动将新锚点添加到ARKit会话时,ARKit会调用此方法,让我们有机会通过在方法结束时返回它来为新创建的锚点提供SceneKit节点。

上面的代码作用如下:

createBillboard()后面,我们添加如下代码:

func addBillboardNode() -> SCNNode? { 
    guard let billboard = billboard else { return nil }
    // 1 
    let rectangle = SCNPlane(width: billboard.plane.width, height: billboard.plane.height)
    // 2 
    let rectangleNode = SCNNode(geometry: rectangle) 
    self.billboard?.billboardNode = rectangleNode
    return rectangleNode
}

上面的代码作用如下:

运行程序,效果如下:

位置有一些偏差,不过后面我们会做优化的。

处理中断

除了这个方向问题,一切看起来都很棒。但是,在将应用程序置于后台之前,有一个小问题可能会被忽视。比如下面这些:

无论新方向是什么,我们都可以在将应用程序发送到后台之前的同一屏幕位置找到该平面。但是,如果我们在应用程序处于活动状态时更改方向,则该平面将移动到其新位置。

ARKit会话中断时,设备停止向ARKit馈送用于确定相对于当前手机位置和方向的节点位置的硬件传感器信息。

虽然可以使用后台处理作为解决方法,但这不是一个合理的解决方案 - 除非只想使用它几秒钟,例如当用户暂时被外部事件分心并且他们在几个内部返回应用程序时秒。但是定期执行此操作会对设备的资源造成巨大损失。

当用户恢复应用程序时,他们必须重复平面检测过程。

实施需要在会话中断时删除广告牌。在ARSCNViewDelegate中有一个SceneKit委托方法:sessionWasInterrupted

此方法已包含在代码中但它是空的。将这行代码添加到其正文中:

removeBillboard()

这将删除广告牌,以便当我们从后台恢复应用程序时,它将返回到相同的状态。

上一章 目录 下一章
上一篇 下一篇

猜你喜欢

热点阅读