在用户环境中找到的矩形形状上创建图像,并增强其展现

代码下载


概述

为了阐述通用的图像识别,这个示例app使用Vision来在用户环境中检测矩形图形,这些矩形图像很有可能是艺术品或照片。在iPhone或者iPad上运行这个app,并将设备的摄像头指向一个电影海报或者挂在墙上的相框。当app检测到一个矩形形状时,你从摄像头信息流中提取出图形定义的像素数据并创建一个图像。

示例app通过应用一个Core ML模型来改变图像的外观从而实现风格变化。为了重复这个动作,你需要使用一个训练过的神经网络来实现实时图像处理。

为了完成在用户环境中增强一个图像,你会用到ARKit的图像追踪特性。ARKit可以保持一个变化后的图像稳定的负载在原始图像上,即使用户在他们的环境中移动设备。当被追踪的图像自主移动时,ARKit同样会继续追踪,例如当app识别到公交车侧面的广告后,公交车开走了。

这个示例app通过使用SceneKit来渲染图像

在用户环境中检测矩形形状

如下所示,你可以使用Vision实时检测摄像头信息流中的矩形。通过使用RectangleDetector来管理一个重复的计时器,其参数updateInterval为0.1秒,来实现每秒检测10次的操作。

init() {
    self.updateTimer = Timer.scheduledTimer(withTimeInterval: updateInterval, repeats: true) { [weak self] _ in
        if let capturedImage = ViewController.instance?.sceneView.session.currentFrame?.capturedImage {
            self?.search(in: capturedImage)
        }
    }
}

由于Vision请求会消耗处理器的资源,对摄像头信息流的检查不应该超过每秒10次,检测矩形的频率过高可能会造成app的帧率下降,同时并不会显著提升app的产出。

当你通过基于ARKit的app实时发送Vision请求时,你应该非常谨慎。在一个请求处理完成之后再发起另外一个请求,确保AR体验运行流畅并没有中断。在搜索方法中,你应该使用isBusy标志位来确保一次只检查一个矩形:

private func search(in pixelBuffer: CVPixelBuffer) {
       guard !isBusy else { return }
       isBusy = true

示例中,当一个Vision请求完成或者失败时设置isBusy标志位为false。

裁剪摄像头信息流成一个观测到的矩形

当Vision在摄像头信息流中找到了一个矩形时,它将会通过VNRectangleObservation 为你提供矩形的精确坐标。将这些坐标传入Core Image透视校正过滤器来对其进行裁剪,只保留矩形形状中的图像数据。

guard let rectangle = request?.results?.first as? VNRectangleObservation else {
    guard let error = error else { return }
    print("Error: Rectangle detection failed - Vision request returned an error. \(error.localizedDescription)")
    return
}
guard let filter = CIFilter(name: "CIPerspectiveCorrection") else {
    print("Error: Rectangle detection failed - Could not create perspective correction filter.")
    return
}
let width = CGFloat(CVPixelBufferGetWidth(currentCameraImage))
let height = CGFloat(CVPixelBufferGetHeight(currentCameraImage))
let topLeft = CGPoint(x: rectangle.topLeft.x * width, y: rectangle.topLeft.y * height)
let topRight = CGPoint(x: rectangle.topRight.x * width, y: rectangle.topRight.y * height)
let bottomLeft = CGPoint(x: rectangle.bottomLeft.x * width, y: rectangle.bottomLeft.y * height)
let bottomRight = CGPoint(x: rectangle.bottomRight.x * width, y: rectangle.bottomRight.y * height)

filter.setValue(CIVector(cgPoint: topLeft), forKey: "inputTopLeft")
filter.setValue(CIVector(cgPoint: topRight), forKey: "inputTopRight")
filter.setValue(CIVector(cgPoint: bottomLeft), forKey: "inputBottomLeft")
filter.setValue(CIVector(cgPoint: bottomRight), forKey: "inputBottomRight")

let ciImage = CIImage(cvPixelBuffer: currentCameraImage).oriented(.up)
filter.setValue(ciImage, forKey: kCIInputImageKey)

guard let perspectiveImage: CIImage = filter.value(forKey: kCIOutputImageKey) as? CIImage else {
    print("Error: Rectangle detection failed - perspective correction filter has no output image.")
    return
}
delegate?.rectangleFound(rectangleContent: perspectiveImage)

当使用概述中的第一张图时,摄像头图像如下所示:

裁剪后的结果:

创建一个关联图像

为了追踪裁剪后的图像做准备,你需要创建一个ARReferenceImage ,该对象为ARKit提供了一切用来在物理环境中定位该图像所需,例如其外观和物理大小。

let possibleReferenceImage = ARReferenceImage(referenceImagePixelBuffer, orientation: .up, physicalWidth: CGFloat(0.5))

ARKit需要关联图像包含足够的细节从而被识别;例如,一个全白的图像是不能被追踪的。为了防止ARKit追踪一个关联图像失败,需要尝试使用它之前验证其有效性。

possibleReferenceImage.validate { [weak self] (error) in
    if let error = error {
        print("Reference image validation failed: \(error.localizedDescription)")
        return
    }

使用ARKit来追踪图像

通过提供关联图像给ARKit,当用户移动他们的设备时,来获取图像在摄像头信息流中的位置更新。要知道这些,需要创建一个图像追踪会话,并将关联图像作为参数传入配置的trackingImages 属性。

let configuration = ARImageTrackingConfiguration()
configuration.maximumNumberOfTrackedImages = 1
configuration.trackingImages = trackingImages
sceneView.session.run(configuration, options: runOptions)

Vision进行关于图像在摄像头信息流的2D空间中位置的初始观察,但是ARKit需要在物理世界的3D空间中解析其位置。当ARKit成功的识别到图像之后,它将会在正确的位置创建一个ARImageAnchor 和一个SceneKit节点。通过将ARKit提供给你的锚点和节点传入一个Altered Image对象来进行保存。

func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
    alteredImage?.add(anchor, node: node)
    setMessageHidden(true)
}

使用Core ML来改变图像的外观

这个示例app打包了一个Core ML模型来进行图像处理。当提供一个输入图像和一个整数索引值后,模型会输出一个该图像的8种不同风格之一的视觉修改版本。输出的具体的样式依赖于传入的数值索引。第一个样式类似于烧过的纸,第二个样式类似于马赛克,还有6个其他的样式如下图所示:

当Vision在用户环境中找到了一个矩形形状后,传入矩形定义的摄像头图像数据到一个新的AlteredImage对象。

guard let newAlteredImage = AlteredImage(rectangleContent, referenceImage: possibleReferenceImage) else { return }

下面的代码展示了如何通过输入数值索引到Core ML模型中完成选取艺术样式并应用到图像中。接下来,调用Core ML模型的predictions(from:options:) 程序来处理图像。

let input = StyleTransferModelInput(image: self.modelInputImage, index: self.styleIndexArray)
let output = try AlteredImage.styleTransferModel.prediction(input: input, options: options)

下图展示了通过样式2来处理图片的结果

在增强现实中展示改变后的图像

为了完成增强现实效果,将改变后的图片覆盖到原始图片上。首先,在ARKit提供的节点上插入一个作为子节点的成像节点。

node.addChildNode(visualizationNode)

当Core ML生产出输出图像时,你调用imageAlteringComplete(_:)方法,并将模型的输出图像传入成像节点的展示函数中,在该函数中设置图像作为成像节点的内容。

func imageAlteringComplete(_ createdImage: CVPixelBuffer) {
    guard fadeBetweenStyles else { return }
    modelOutputImage = createdImage
    visualizationNode.display(createdImage)
}

当SceneKit展示成像节点的内容时,会将其叠加在原始图片上。在上述图像的情况中,下面的截图展示了用户设备的输出结果。

持续更新图片的外观

示例展示了不断切换艺术样式时的实时图像处理。通过调用selectNextStyle方法,可以对原始图像进行持续改变。styleIndex是Core ML模型的整形输入值用来决定输出的样式。

func selectNextStyle() {
    styleIndex = (styleIndex + 1) % numberOfStyles
}

示例的VisualizationNode在两个不同样式的图像之间逐渐变暗,创造出追踪图像逐渐变成新的外观的效果。可以通过定义两个SceneKit节点来实现这个效果。一个节点展示当前的变化图像,另一个展示之前的变化图像。

private let currentImage: SCNNode
private let previousImage: SCNNode

通过运行一个透明度动画在两个节点之间完成逐渐变暗:

SCNTransaction.begin()
SCNTransaction.animationDuration = fadeDuration
currentImage.opacity = 1.0
previousImage.opacity = 0.0
SCNTransaction.completionBlock = {
    self.delegate?.visualizationNodeDidFinishFade(self)
}
SCNTransaction.commit()

当动画完成后,再一次通过调用createAlteredImage方法来用下一个艺术样式来改变原始图像。

func visualizationNodeDidFinishFade(_ visualizationNode: VisualizationNode) {
    guard fadeBetweenStyles, anchor != nil else { return }
    selectNextStyle()
    createAlteredImage()
}

响应图像追踪的更新

作为图像追踪特性的一部分,ARKit在整个AR会话中持续寻找图像。如果图像本身发生了移动,ARKit将会用对应的图像在物理环境中的新的位置来更新ARImageAnchor ,并通过调用你的代理方法的renderer(_:didUpdate:for:) 方法来通知app这些变化。

func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
    alteredImage?.update(anchor)
}

示例app一次只会追踪一个图像。为了这样做,当app追踪的图像不再可见时,需要使当前的图像追踪会话失效。转而使得Vision在摄像头信息流中开始寻找新的矩形形状。

func update(_ anchor: ARAnchor) {
    if let imageAnchor = anchor as? ARImageAnchor, self.anchor == anchor {
        self.anchor = imageAnchor
        // Reset the timeout if the app is still tracking an image.
        if imageAnchor.isTracked {
            resetImageTrackingTimeout()
        }
    }