LiDAR-Daten in ARKit 3.5

LiDAR-Daten in ARKit 3.5
24/7/2020

Eines der vielen interessanten Projekte, an denen wir derzeit bei SABO Mobile IT arbeiten, ist eine auf dem ARKit basierende iOS-App für unseren langjährigen Kunden Audi in Zusammenarbeit mit NavVis. Wir sammeln die Informationen über die (zukünftigen/geplanten) Standorte realer Objekte und stellen sie in Augmented Reality so genau positioniert dar, wie es uns das ARKit erlaubt. Auf dem Weg dorthin sind wir auf zahlreiche technische Herausforderungen gestoßen, von denen die meisten mehr oder weniger mit der Positionierung zu tun hatten. Die beträchtliche Ungenauigkeit des ARKits war das größte Problem. Dies hat viele Stunden Analyse und die Durchführung von Experimenten erfordert, um das Projekt durchzuführen.

Wir haben ein neues iPad Pro mit LiDAR und ARKit 3.5 getestet. Wir wollten erfahren, welche Art von Daten uns ARKit liefern kann und was sich seit der letzten Version genau geändert hat. Die Ergebnisse möchte ich gerne mit Ihnen teilen.

ARKit 3.5

Anker

ARKit ermöglicht die Abbildung von virtuellen Objekten auf Oberflächen der realen Welt über Anker. Jeder Anker trägt eine Information über seine Transformation (Position, Orientierung, Maßstab) im virtuellen 3D-Raum. Durch die Verwendung dieser Informationen sind wir in der Lage, unsere Objekte in einer gut passenden Position/Orientierung/Maßstab darzustellen. Wenn diese Objekte also über einen von der Kamera stammenden Videostream gerendert werden, sieht es so aus, als wären sie wirklich da.

Die erste Version erlaubte es den Entwicklern lediglich Objekte auf horizontale Ebenen zu legen. Schritt für Schritt wurde die API erweitert, und jetzt (in der letzten größeren Version zum Zeitpunkt der Verfassung dieses Artikels, nämlich ARKit 3) haben wir 4 Arten von Ankern verwendet: ARPlaneAnchor (vertikale und horizontale Ebenen), ARImageAnchor (vortrainiertes Bild), ARObjectAnchor (vortrainiertes 3D-Objekt) und ARFaceAnchor (menschliches Gesicht).

Mit dem ARKit 3.5 wurde ein neuer Ankertyp eingeführt – ARMeshAnchor. Wie Sie es vielleicht schon schon dem Namen entnommen haben, transformiert ARMeshAnchor nicht nur – durch das Sammeln von Daten aus LiDAR. Ebenso werden Informationen über die Geometrie der Umgebung bereitgestellt.

Rohdaten

Der Zugriff auf die von ARMeshAnchor bereitgestellte Geometrie erfolgt über var geometry: ARMeshGeometry { get } Eigenschaft.
Schauen wir uns nun die neue Struktur ARMeshGeometry genauer an:(https://developer.apple.com/documentation/arkit/armeshgeometry):

{% c-block language="swift" %}
/**
A three-dimensional shape that represents the geometry of a mesh.
*/
@available(iOS 13.4, *)
open class ARMeshGeometry : NSObject, NSSecureCoding {
       
       /*
       The vertices of the mesh.
       */
       open var vertices: ARGeometrySource { get }
       /*
       The normals of the mesh.
       */
       open var normals: ARGeometrySource { get }
       /*
       A list of all faces in the mesh.
       */
       open var faces: ARGeometryElement { get }

       /*
       Classification for each face in the mesh.
       */
       open var classification: ARGeometrySource? { get }
}
{% c-block-end %}

Scheitelpunkte, Scheitelpunktnormale und Klassifikation werden durch eine neue Klasse ARGeometrySource dargestellt. Laut Apple-Dokumentation handelt es sich dabei um Polygonnetz-Daten in einem pufferbasierten Array. Der Datentyp der im Puffer dargestellten Daten wird als MTLVertexformat beschrieben.

ARGeometrySource verweist also auf ein Stück MTLBuffer, bei dem es sich um ein Array von Vektoren mit count Elementen handelt. Der Vektor selbst ist ebenfalls ein Array mit fester Länge (stride Bytes), beschrieben in format.

Mit Hilfe einer Cut-and-Try-Methode fand ich heraus, dass für Scheitelpunkte und Scheitelpunktnormale das Format MTLVertexFormat.float3 ist, (d.h. 3 Floats, die die X-, Y- und Z-Koordinaten von Vertex/Vektor darstellen).

Das Format für die Klassifizierung ist MTLVertexFormat.uchar, das den Rohwert der ARMeshClassification-Aufzählung darstellt.

Was ich ebenfalls herausfand war, dass die Anzahl der Scheitelpunktnormale der Anzahl der Scheitelpunkte entspricht, nicht der Anzahl der Flächen. Diese Tatsache stimmt weder mit der Dokumentation überein, in der die Eigenschaft der Scheitelpunktnormale als Strahlen beschrieben wird, die definieren, welche Richtung für jede Fläche außen liegt, noch entspricht sie der allgemeinen Bedeutung eines Scheitelpunktnormals.

Der nächste Datentyp ist ARGeometryElement, das für die Beschreibung von Flächen verwendet wird. Er enthält auch einen Metal Puffer: MTLVertexFormat. Der Puffer enthält ein Array von Vertex-Indizes. Jedes Element im Puffer stellt eine Fläche dar. Jede F-Fläche wird durch eine feste Anzahl von Zahlen dargestellt (indexCountPerPrimitive), und jede Zahl ist ein Scheitelpunktindex.

Praktischer Ansatz

Für mich war es interessant zu untersuchen, wie ARKit ARMeshAnchor den Feature-Punkten zuordnet. RealityKit bietet eine Visualisierung für Debugging-Zwecke, aber es zeigt nur ein Gitter, das auf den Geometrien aller Anker basiert. Wie die Geometrie eines einzelnen Ankers aussieht, lässt sich daraus nicht ableiten.

Prozedurales Polygonnetz

Die Erzeugung und Darstellung eines virtuellen Objekts auf der Grundlage von Netzdaten aus LiDAR in Echtzeit kann sehr cool sein, nicht wahr?

Leider werden die prozeduralen Polygonnetze im RealityKit immer noch nicht vollständig unterstützt. Sie können definitiv Primitive erzeugen, indem Sie MeshSource.generateBox und ähnliches verwenden, aber nicht ein komplexes Netz. Die in ARMeshAnchorbereitgestellte Geometrie ist eine Reihe von Flächen, und man wird sie nicht durch die Verwendung von Primitiven darstellen können. Es ist keine Überraschung, dass RealityKit noch recht neu ist und sich auf dem Weg zur Reife befindet.

Es gibt jedoch noch einen Weg im RealityKit. Sie können ein MDLAsset über das Model I/O-Framework generieren, es in das usdz-Format exportieren und über ModelEntity.load(contentsOf:withName:) wieder in das RealityKit importieren. Allerdings kann es im Echtzeit-Anwendungsfall aufgrund von I/O-Operationen mit dem Dateisystem zu hohen Latenzzeiten kommen.

Bei der Erzeugung von Laufzeitnetzen war ich wirklich erfolgreich mit einem SceneKit – damit kann  SCNGeometrie dynamisch erstellt und einem SCNNode zugewiesen werden.

Mit ein paar praktischen Erweiterungen kann der Code so aussehen:

{% c-block language="swift" %}
/* MARK: **- ARSCNViewDelegate** */

func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
/*
Create a node for a new ARMeshAnchor
We are only interested in anchors that provide mesh
*/
guard let meshAnchor = anchor as? ARMeshAnchor else {
return nil
}
/* Generate a SCNGeometry (explained further) */
let geometry = SCNGeometry(arGeometry: meshAnchor.geometry)

/* Let's assign random color to each ARMeshAnchor/SCNNode be able to distinguish them in demo */
geometry.firstMaterial?.diffuse.contents = colorizer.assignColor(to: meshAnchor.identifier)

/* Create node & assign geometry */
let node = SCNNode()
node.name = "DynamicNode-\(meshAnchor.identifier)"
node.geometry = geometry
return node
}

func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
/* Update the node's geometry when mesh or position changes */
guard let meshAnchor = anchor as? ARMeshAnchor else {
return
}
/* Generate a new geometry */
let newGeometry = SCNGeometry(arGeometry: meshAnchor.geometry)  /* regenerate geometry */

/* Assign the same color (colorizer stores id <-> color map internally) */
newGeometry.firstMaterial?.diffuse.contents = colorizer.assignColor(to: meshAnchor.identifier)

/* Replace node's geometry with a new one */
node.geometry = newGeometry
}
{% c-block-end %}

Die Umwandlung von ARMeshGeometry in SCNGeometry ist ziemlich einfach, da die Strukturen sehr ähnlich sind:

{% c-block language="swift" %}
extension  SCNGeometry {
       convenience init(arGeometry: ARMeshGeometry) {
              let verticesSource = SCNGeometrySource(arGeometry.vertices, semantic: .vertex)
              let normalsSource = SCNGeometrySource(arGeometry.normals, semantic: .normal)
              let faces = SCNGeometryElement(arGeometry.faces)
              self.init(sources: [verticesSource, normalsSource], elements: [faces])
       }
}
extension  SCNGeometrySource {
       convenience init(_ source: ARGeometrySource, semantic: Semantic) {
              self.init(buffer: source.buffer, vertexFormat: source.format, semantic: semantic, vertexCount: source.count, dataOffset: source.offset, dataStride: source.stride)
       }
}
extension  SCNGeometryElement {
       convenience init(_ source: ARGeometryElement) {
              let pointer = source.buffer.contents()
              let byteCount = source.count * source.indexCountPerPrimitive * source.bytesPerIndex
              let data = Data(bytesNoCopy: pointer, count: byteCount, deallocator: .none)
              self.init(data: data, primitiveType: .of(source.primitiveType), primitiveCount: source.count, bytesPerIndex: source.bytesPerIndex)
       }
}
extension  SCNGeometryPrimitiveType {
       static  func  of(_ type: ARGeometryPrimitiveType) -> SCNGeometryPrimitiveType {
              switch type {
              case .line:
                      return .line
              case .triangle:
                      return .triangles
              }
       }
}
{% c-block-end %}

Und hier ist das Ergebnis:

Wie wir sehen können, produziert das ARKit "quadratische" Polygonnetze, ungefähr 1m x 1m. Einige von ihnen können sich überschneiden.

Schlussfolgerung

Ähnlich wie die frühen RealityKit-Versionen (eine neuer Art und Weise primär mit Augmented Reality zu arbeiten), in denen viele Funktionen fehlten, kam die neue Version von ARKit 3.5 mit einer einfachen und recht kleinen API, die eine wichtige Verbesserung abdeckt. Sie enthält auch einen weiteren Anker Typ und ein einige neue Klassen im Computergrafikstil. Ich persönlich fand es interessanter, diese neuen Entitäten auf praktische Art und Weise auszuprobieren. Dies hat mir geholfen herauszufinden, wie man damit arbeitet, Es hat mir ebenfalls interessante Fakten aufgezeigt, die in der Dokumentation nicht erwähnt werden. Dieser erste Ergebnis das ich beschrieben habe, kann als eine Vorgehensweise angesehen werden, wie Apple ein komplexes Problem vereinfacht. Aber dies ist nur der erste Teil von dem, was ich entdeckt habe. Bleiben Sie dran und erfahren Sie mehr zu diesem Thema.

Teilen:
Nikita ist ein Senior Frontend-Entwickler, der sich seit 2006 intensiv mit Programmierung beschäftigt. Nicht nur mit der Entwicklung komplexer SPA-Webanwendungen, sondern auch mit kleinen und mittleren iOS-Anwendungen. Schneller Lerner, innovativer Geist, sympathischer Teamkollege und angeblich der größte Geek bei SABO.

Weitere Artikel dieses Autors

Article collaborators

Pavel Zdeněk

SABO Mobile IT

Für unsere Kunden aus der Industrie entwickeln wir spezialisierte Software zur Umsetzung von Industry 4.0. IoT, Machine Learning und Künstliche Intelligenz ermöglichen uns, signifikante Effizienzsteigerungen bei unseren Kunden zu erzielen.
Über uns