Tutorial iOS ARKit: Desenați în aer cu degetele goale
Publicat: 2022-03-11Recent, Apple a anunțat noua sa bibliotecă de realitate augmentată (AR), numită ARKit. Pentru mulți, părea doar o altă bibliotecă AR bună, nu un disruptor tehnologic de care să-i pese. Cu toate acestea, dacă aruncați o privire la progresul AR din ultimii doi ani, nu ar trebui să vă grăbiți să trageți astfel de concluzii.
În această postare, vom crea un exemplu de proiect ARKit distractiv folosind iOS ARKit. Utilizatorul își va plasa degetele pe o masă ca și cum ar ține un pix, va apăsa pe miniatură și va începe să deseneze. Odată terminat, utilizatorul își va putea transforma desenul într-un obiect 3D, așa cum se arată în animația de mai jos. Codul sursă complet pentru exemplul nostru ARKit iOS este disponibil pe GitHub.
De ce ar trebui să ne pese de iOS ARKit acum?
Fiecare dezvoltator cu experiență este probabil conștient de faptul că AR este un concept vechi. Putem stabili prima dezvoltare serioasă a AR până la momentul în care dezvoltatorii au acces la cadre individuale de la camerele web. Aplicațiile la acea vreme erau de obicei folosite pentru a vă transforma fața. Cu toate acestea, omenirea nu a întârziat mult până să-și dea seama că transformarea fețelor în iepurași nu era una dintre nevoile lor cele mai iminente și, în curând, hype-ul a dispărut!
Cred că AR a lipsit întotdeauna două salturi tehnologice cheie pentru a o face utilă: uzabilitate și imersiune. Dacă ați urmărit alte hype-uri AR, veți observa acest lucru. De exemplu, hype-ul AR a declanșat din nou când dezvoltatorii au avut acces la cadre individuale de la camerele mobile. Pe lângă revenirea puternică a marilor transformatori iepurași, am văzut un val de aplicații care aruncă obiecte 3D pe coduri QR imprimate. Dar nu au decolat niciodată ca concept. Nu erau realitate augmentată, ci mai degrabă coduri QR augmentate.
Apoi Google ne-a surprins cu o bucată de science fiction, Google Glass. Au trecut doi ani și, în momentul în care se aștepta ca acest produs uimitor să prindă viață, era deja mort! Mulți critici au analizat motivele eșecului Google Glass, punând vina pe orice, de la aspecte sociale până la abordarea plictisitoare a Google la lansarea produsului. Cu toate acestea, ne pasă în acest articol dintr-un motiv special - imersiunea în mediu. În timp ce Google Glass a rezolvat problema de utilizare, nu era nimic altceva decât o imagine 2D trasată în aer.
Titanii tehnologiei precum Microsoft, Facebook și Apple au învățat această lecție dură pe de rost. În iunie 2017, Apple și-a anunțat frumoasa bibliotecă iOS ARKit, făcând imersiunea sa principala prioritate. Deținerea unui telefon este încă un blocant important pentru experiența utilizatorului, dar lecția Google Glass ne-a învățat că hardware-ul nu este problema.
Cred că ne îndreptăm către un nou vârf de hype AR foarte curând și, cu acest nou pivot semnificativ, își poate găsi în cele din urmă piața de origine, permițând mai multor dezvoltări de aplicații AR să devină mainstream. Acest lucru înseamnă, de asemenea, că fiecare companie de dezvoltare a aplicațiilor de realitate augmentată va putea accesa ecosistemul și baza de utilizatori Apple.
Dar destulă istorie, haideți să ne murdărim mâinile cu cod și să vedem realitatea augmentată Apple în acțiune!
Caracteristici de imersie ARKit
ARKit oferă două caracteristici principale; prima este locația camerei în spațiul 3D și a doua este detectarea planului orizontal. Pentru a realiza primul, ARKit presupune că telefonul dvs. este o cameră care se mișcă în spațiul 3D real, astfel încât aruncarea unui obiect virtual 3D în orice punct va fi ancorată în acel punct în spațiul 3D real. Și pentru cei din urmă, ARKit detectează planuri orizontale precum mesele, astfel încât să puteți plasa obiecte pe el.
Deci, cum reușește ARKit acest lucru? Acest lucru se realizează printr-o tehnică numită Visual Inertial Odometry (VIO). Nu-ți face griji, la fel cum antreprenorii își găsesc plăcerea în numărul de chicoteli pe care le chicoti când îți dai seama sursa din spatele numelui lor startup, cercetătorii o găsesc în numărul de zgârieturi de cap pe care le faci încercând să descifreze orice termen cu care vin atunci când denumindu-și invențiile - așa că haideți să le permitem să se distreze și să mergem mai departe.
VIO este o tehnică prin care cadrele camerei sunt fuzionate cu senzori de mișcare pentru a urmări locația dispozitivului în spațiul 3D. Urmărirea mișcării din cadrele camerei se face prin detectarea caracteristicilor sau, cu alte cuvinte, a punctelor de margine din imagine cu contrast ridicat - cum ar fi marginea dintre o vază albastră și o masă albă. Detectând cât de mult s-au mutat aceste puncte unul față de celălalt de la un cadru la altul, se poate estima unde se află dispozitivul în spațiul 3D. De aceea, ARKit nu va funcționa corect atunci când este plasat în fața unui perete alb fără caracteristici sau când dispozitivul se mișcă foarte repede, rezultând imagini neclare.
Noțiuni introductive cu ARKit în iOS
În momentul scrierii acestui articol, ARKit face parte din iOS 11, care este încă în versiune beta. Așadar, pentru a începe, trebuie să descărcați iOS 11 Beta pe iPhone 6s sau mai sus și noul Xcode Beta. Putem începe un nou proiect ARKit din New > Project > Augmented Reality App . Cu toate acestea, mi s-a părut mai convenabil să încep acest tutorial de realitate augmentată cu eșantionul oficial Apple ARKit, care oferă câteva blocuri de cod esențiale și este deosebit de util pentru detectarea avioanelor. Deci, să începem cu acest exemplu de cod, să explicăm mai întâi punctele principale din el și apoi să îl modificăm pentru proiectul nostru.
În primul rând, ar trebui să stabilim ce motor vom folosi. ARKit poate fi folosit cu Sprite SceneKit sau Metal. În exemplul Apple ARKit, folosim iOS SceneKit, un motor 3D furnizat de Apple. Apoi, trebuie să setăm o vizualizare care va reda obiectele noastre 3D. Acest lucru se face prin adăugarea unei vizualizări de tip ARSCNView .
ARSCNView este o subclasă a vederii principale SceneKit numită SCNView , dar extinde vizualizarea cu câteva caracteristici utile. Redă fluxul video live de la camera dispozitivului ca fundal al scenei, în timp ce potrivește automat spațiul SceneKit cu lumea reală, presupunând că dispozitivul este o cameră în mișcare în această lume.
ARSCNView nu realizează procesarea AR pe cont propriu, dar necesită un obiect de sesiune AR care gestionează camera dispozitivului și procesarea mișcării. Deci, pentru a începe, trebuie să atribuim o nouă sesiune:
self.session = ARSession() sceneView.session = session sceneView.delegate = self setupFocusSquare()Ultima linie de mai sus adaugă un indicator vizual care ajută utilizatorul să descrie vizual starea detectării avionului. Focus Square este furnizat de codul exemplu, nu de biblioteca ARKit și este unul dintre principalele motive pentru care am început cu acest cod exemplu. Puteți găsi mai multe despre el în fișierul readme inclus în exemplul de cod. Următoarea imagine arată un pătrat de focalizare proiectat pe un tabel:
Următorul pas este să porniți sesiunea ARKit. Este logic să repornim sesiunea de fiecare dată când apare vizualizarea, deoarece nu putem folosi informațiile despre sesiunea anterioară dacă nu mai urmărim utilizatorul. Deci, vom începe sesiunea în viewDidAppear:
override func viewDidAppear(_ animated: Bool) { let configuration = ARWorldTrackingSessionConfiguration() configuration.planeDetection = .horizontal session.run(configuration, options: [.resetTracking, .removeExistingAnchors]) }În codul de mai sus, începem prin a seta configurația sesiunii ARKit pentru a detecta planuri orizontale. În momentul scrierii acestui articol, Apple nu oferă alte opțiuni decât aceasta. Dar se pare că sugerează detectarea unor obiecte mai complexe în viitor. Apoi, începem să rulăm sesiunea și ne asigurăm că resetăm urmărirea.
În cele din urmă, trebuie să actualizăm Focus Square ori de câte ori poziția camerei, adică orientarea sau poziția reală a dispozitivului, se schimbă. Acest lucru se poate face în funcția de delegat de randare a SCNView, care este apelată de fiecare dată când va fi redat un nou cadru al motorului 3D:
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { updateFocusSquare() }Până în acest moment, dacă rulați aplicația, ar trebui să vedeți pătratul de focalizare deasupra fluxului camerei care caută un plan orizontal. În secțiunea următoare, vom explica cum sunt detectate avioanele și cum putem poziționa pătratul de focalizare în consecință.
Detectarea avioanelor în ARKit
ARKit poate detecta avioane noi, le poate actualiza pe cele existente sau le poate elimina. Pentru a gestiona planurile într-un mod la îndemână, vom crea un nod SceneKit fals care conține informații despre poziția planului și o referință la pătratul de focalizare. Planurile sunt definite în direcția X și Z, unde Y este normalul suprafeței, adică ar trebui să păstrăm întotdeauna pozițiile nodurilor noastre de desen în aceeași valoare Y a planului dacă vrem să îl facem să arate ca și cum ar fi imprimat pe plan. .
Detectarea avioanelor se face prin funcțiile de apel invers oferite de ARKit. De exemplu, următoarea funcție de apel invers este apelată ori de câte ori este detectat un nou avion:
var planes = [ARPlaneAnchor: Plane]() func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { if let planeAnchor = anchor as? ARPlaneAnchor { serialQueue.async { self.addPlane(node: node, anchor: planeAnchor) self.virtualObjectManager.checkIfObjectShouldMoveOntoPlane(anchor: planeAnchor, planeAnchorNode: node) } } } func addPlane(node: SCNNode, anchor: ARPlaneAnchor) { let plane = Plane(anchor) planes[anchor] = plane node.addChildNode(plane) } ... class Plane: SCNNode { var anchor: ARPlaneAnchor var focusSquare: FocusSquare? init(_ anchor: ARPlaneAnchor) { self.anchor = anchor super.init() } ... } Funcția de apel invers ne oferă doi parametri, anchor și node . node este un nod SceneKit normal plasat la poziția și orientarea exactă a planului. Nu are geometrie, deci este invizibil. Îl folosim pentru a adăuga propriul nod plan, care este, de asemenea, invizibil, dar deține informații despre orientarea și poziția planului în anchor .
Deci, cum sunt salvate poziția și orientarea în ARPlaneAnchor ? Poziția, orientarea și scara sunt toate codificate într-o matrice 4x4. Dacă am șansa de a alege un concept de matematică pe care să îl înveți, acesta va fi fără îndoială matrice. Oricum, putem ocoli acest lucru descriind această matrice 4x4 după cum urmează: O matrice bidimensională strălucitoare care conține numere în virgulă mobilă 4x4. Prin înmulțirea acestor numere într-un anumit mod cu un vârf 3D, v1, în spațiul său local, rezultă un nou vârf 3D, v2, care reprezintă v1 în spațiul mondial. Deci, dacă v1 = (1, 0, 0) în spațiul său local și dorim să-l plasăm la x = 100 în spațiul mondial, v2 va fi egal cu (101, 0, 0) în raport cu spațiul mondial. Desigur, matematica din spatele acestui lucru devine mai complexă atunci când adăugăm rotații în jurul axelor, dar vestea bună este că ne putem descurca fără a o înțelege (recomand cu căldură să verificați secțiunea relevantă din acest articol excelent pentru o explicație aprofundată a acestui concept ).
checkIfObjectShouldMoveOntoPlane verifică dacă avem deja obiecte desenate și verifică dacă axa y a tuturor acestor obiecte se potrivește cu cea a planurilor nou detectate.

Acum, înapoi la updateFocusSquare() , descris în secțiunea anterioară. Dorim să păstrăm pătratul de focalizare în centrul ecranului, dar proiectat pe cel mai apropiat plan detectat. Codul de mai jos demonstrează acest lucru:
func updateFocusSquare() { let worldPos = worldPositionFromScreenPosition(screenCenter, self.sceneView) self.focusSquare?.simdPosition = worldPos } func worldPositionFromScreenPosition(_ position: CGPoint, in sceneView: ARSCNView) -> float3? { let planeHitTestResults = sceneView.hitTest(position, types: .existingPlaneUsingExtent) if let result = planeHitTestResults.first { return result.worldTransform.translation } return nil } sceneView.hitTest caută planuri din lumea reală corespunzătoare unui punct 2D din vizualizarea ecranului prin proiectarea acestui punct 2D la cel mai apropiat plan de dedesubt. result.worldTransform este o matrice 4x4 care deține toate informațiile de transformare ale planului detectat, în timp ce result.worldTransform.translation este o funcție utilă care returnează numai poziția.
Acum, avem toate informațiile de care avem nevoie pentru a arunca un obiect 3D pe suprafețele detectate, având în vedere un punct 2D pe ecran. Deci, să începem să desenăm.
Desen
Să explicăm mai întâi abordarea desenării formelor care urmează degetul unui om în viziunea computerizată. Desenarea formelor se face prin detectarea fiecărei locații noi pentru degetul în mișcare, scăparea unui vârf în acea locație și conectând fiecare vârf cu cel anterior. Vârfurile pot fi conectate printr-o linie simplă sau printr-o curbă Bezier dacă avem nevoie de o ieșire lină.
Pentru simplitate, vom urma o abordare puțin naivă pentru desen. Pentru fiecare nouă locație a degetului, vom arunca o cutie foarte mică cu colțuri rotunjite și aproape zero înălțime pe planul detectat. Va apărea ca și cum ar fi un punct. Odată ce utilizatorul termină desenul și selectează butonul 3D, vom modifica înălțimea tuturor obiectelor aruncate în funcție de mișcarea degetului utilizatorului.
Următorul cod arată clasa PointNode care reprezintă un punct:
let POINT_SIZE = CGFloat(0.003) let POINT_HEIGHT = CGFloat(0.00001) class PointNode: SCNNode { static var boxGeo: SCNBox? override init() { super.init() if PointNode.boxGeo == nil { PointNode.boxGeo = SCNBox(width: POINT_SIZE, height: POINT_HEIGHT, length: POINT_SIZE, chamferRadius: 0.001) // Setup the material of the point let material = PointNode.boxGeo!.firstMaterial material?.lightingModel = SCNMaterial.LightingModel.blinn material?.diffuse.contents = UIImage(named: "wood-diffuse.jpg") material?.normal.contents = UIImage(named: "wood-normal.png") material?.specular.contents = UIImage(named: "wood-specular.jpg") } let object = SCNNode(geometry: PointNode.boxGeo!) object.transform = SCNMatrix4MakeTranslation(0.0, Float(POINT_HEIGHT) / 2.0, 0.0) self.addChildNode(object) } . . . }Veți observa în codul de mai sus că traducem geometria de-a lungul axei y cu jumătate din înălțime. Motivul pentru aceasta este să vă asigurați că partea de jos a obiectului este întotdeauna la y = 0 , astfel încât să apară deasupra planului.
Apoi, în funcția de apel invers de redare a SceneKit, vom desena un indicator care acționează ca un punct de vârf al stiloului, folosind aceeași clasă PointNode . Vom arunca un punct în acea locație dacă desenul este activat sau vom ridica desenul într-o structură 3D dacă modul 3D este activat:
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { updateFocusSquare() // Setup a dot that represents the virtual pen's tippoint if (self.virtualPenTip == nil) { self.virtualPenTip = PointNode(color: UIColor.red) self.sceneView.scene.rootNode.addChildNode(self.virtualPenTip!) } // Draw if let screenCenterInWorld = worldPositionFromScreenPosition(self.screenCenter, self.sceneView) { // Update virtual pen position self.virtualPenTip?.isHidden = false self.virtualPenTip?.simdPosition = screenCenterInWorld // Draw new point if (self.inDrawMode && !self.virtualObjectManager.pointNodeExistAt(pos: screenCenterInWorld)){ let newPoint = PointNode() self.sceneView.scene.rootNode.addChildNode(newPoint) self.virtualObjectManager.loadVirtualObject(newPoint, to: screenCenterInWorld) } // Convert drawing to 3D if (self.in3DMode ) { if self.trackImageInitialOrigin != nil { DispatchQueue.main.async { let newH = 0.4 * (self.trackImageInitialOrigin!.y - screenCenterInWorld.y) / self.sceneView.frame.height self.virtualObjectManager.setNewHeight(newHeight: newH) } } else { self.trackImageInitialOrigin = screenCenterInWorld } } } virtualObjectManager este o clasă care gestionează punctele desenate. În modul 3D, estimăm diferența față de ultima poziție și creștem/scădem înălțimea tuturor punctelor cu acea valoare.
Până acum, desenăm pe suprafața detectată presupunând că stiloul virtual se află în centrul ecranului. Acum, pentru partea distractivă - detectarea degetului utilizatorului și utilizarea acestuia în locul centrului ecranului.
Detectarea vârfului degetului utilizatorului
Una dintre bibliotecile interesante pe care Apple le-a introdus în iOS 11 este Vision Framework. Oferă câteva tehnici de viziune computerizată într-un mod destul de la îndemână și eficient. În special, vom folosi tehnica de urmărire a obiectelor pentru tutorialul nostru de realitate augmentată. Urmărirea obiectelor funcționează după cum urmează: În primul rând, îi oferim o imagine și coordonatele unui pătrat în limitele imaginii pentru obiectul pe care vrem să-l urmărim. După aceea, apelăm o funcție pentru a inițializa urmărirea. În cele din urmă, introducem o nouă imagine în care poziția acelui obiect s-a schimbat și rezultatul analizei operației anterioare. Având în vedere asta, ne va returna noua locație a obiectului.
Vom folosi un mic truc. Vom cere utilizatorului să-și sprijine mâna pe masă ca și cum ar ține un stilou și să se asigure că miniața lor este orientată spre cameră, după care ar trebui să apese pe miniatură de pe ecran. Sunt două puncte care trebuie detaliate aici. În primul rând, miniatura ar trebui să aibă suficiente caracteristici unice pentru a fi urmărită prin contrastul dintre miniatura albă, piele și masă. Aceasta înseamnă că pigmentul mai închis al pielii va avea ca rezultat o urmărire mai fiabilă. În al doilea rând, deoarece utilizatorul își sprijină mâinile pe masă și deoarece detectăm deja masa ca un plan, proiectarea locației miniaturii din vizualizarea 2D în mediul 3D va duce la locația aproape exactă a degetului pe masa.
Următoarea imagine arată punctele caracteristice care ar putea fi detectate de biblioteca Vision:
Vom inițializa urmărirea miniaturilor printr-un gest de atingere, după cum urmează:
// MARK: Object tracking fileprivate var lastObservation: VNDetectedObjectObservation? var trackImageBoundingBox: CGRect? let trackImageSize = CGFloat(20) @objc private func tapAction(recognizer: UITapGestureRecognizer) { lastObservation = nil let tapLocation = recognizer.location(in: view) // Set up the rect in the image in view coordinate space that we will track let trackImageBoundingBoxOrigin = CGPoint(x: tapLocation.x - trackImageSize / 2, y: tapLocation.y - trackImageSize / 2) trackImageBoundingBox = CGRect(origin: trackImageBoundingBoxOrigin, size: CGSize(width: trackImageSize, height: trackImageSize)) let t = CGAffineTransform(scaleX: 1.0 / self.view.frame.size.width, y: 1.0 / self.view.frame.size.height) let normalizedTrackImageBoundingBox = trackImageBoundingBox!.applying(t) // Transfrom the rect from view space to image space guard let fromViewToCameraImageTransform = self.sceneView.session.currentFrame?.displayTransform(withViewportSize: self.sceneView.frame.size, orientation: UIInterfaceOrientation.portrait).inverted() else { return } var trackImageBoundingBoxInImage = normalizedTrackImageBoundingBox.applying(fromViewToCameraImageTransform) trackImageBoundingBoxInImage.origin.y = 1 - trackImageBoundingBoxInImage.origin.y // Image space uses bottom left as origin while view space uses top left lastObservation = VNDetectedObjectObservation(boundingBox: trackImageBoundingBoxInImage) } Cea mai dificilă parte de mai sus este cum să convertiți locația atingerii din spațiul de coordonate UIView în spațiul de coordonate a imaginii. ARKit ne oferă matricea displayTransform care convertește din spațiu de coordonate imagine într-un spațiu de coordonate de vizualizare, dar nu invers. Deci, cum putem face invers? Folosind inversul matricei. Am încercat cu adevărat să minimizez utilizarea matematicii în această postare, dar uneori este inevitabil în lumea 3D.
Apoi, în redare, vom introduce o nouă imagine pentru a urmări noua locație a degetului:
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { // Track the thumbnail guard let pixelBuffer = self.sceneView.session.currentFrame?.capturedImage, let observation = self.lastObservation else { return } let request = VNTrackObjectRequest(detectedObjectObservation: observation) { [unowned self] request, error in self.handle(request, error: error) } request.trackingLevel = .accurate do { try self.handler.perform([request], on: pixelBuffer) } catch { print(error) } . . . }Odată ce urmărirea obiectelor este finalizată, va apela o funcție de apel invers în care vom actualiza locația miniaturii. De obicei, este inversul codului scris în dispozitivul de recunoaștere a apei:
fileprivate func handle(_ request: VNRequest, error: Error?) { DispatchQueue.main.async { guard let newObservation = request.results?.first as? VNDetectedObjectObservation else { return } self.lastObservation = newObservation var trackImageBoundingBoxInImage = newObservation.boundingBox // Transfrom the rect from image space to view space trackImageBoundingBoxInImage.origin.y = 1 - trackImageBoundingBoxInImage.origin.y guard let fromCameraImageToViewTransform = self.sceneView.session.currentFrame?.displayTransform(withViewportSize: self.sceneView.frame.size, orientation: UIInterfaceOrientation.portrait) else { return } let normalizedTrackImageBoundingBox = trackImageBoundingBoxInImage.applying(fromCameraImageToViewTransform) let t = CGAffineTransform(scaleX: self.view.frame.size.width, y: self.view.frame.size.height) let unnormalizedTrackImageBoundingBox = normalizedTrackImageBoundingBox.applying(t) self.trackImageBoundingBox = unnormalizedTrackImageBoundingBox // Get the projection if the location of the tracked image from image space to the nearest detected plane if let trackImageOrigin = self.trackImageBoundingBox?.origin { self.lastFingerWorldPos = self.virtualObjectManager.worldPositionFromScreenPosition(CGPoint(x: trackImageOrigin.x - 20.0, y: trackImageOrigin.y + 40.0), in: self.sceneView) } } } În cele din urmă, vom folosi self.lastFingerWorldPos în loc de centrul ecranului când desenăm și am terminat.
ARKit și viitorul
În această postare, am demonstrat cum AR ar putea fi captivantă prin interacțiunea cu degetele utilizatorilor și cu tabele din viața reală. Cu mai multe progrese în viziunea computerizată și adăugând mai mult hardware prietenos cu AR la gadgeturi (cum ar fi camerele de adâncime), putem avea acces la structura 3D a tot mai multe obiecte din jurul nostru.
Deși nu a fost încă lansat în masă, merită menționat faptul că Microsoft este foarte serios să câștige cursa AR prin dispozitivul său Hololens, care combină hardware adaptat AR cu tehnici avansate de recunoaștere a mediului 3D. Puteți aștepta să vedeți cine va câștiga această cursă sau puteți face parte din ea dezvoltând acum aplicații reale de realitate augmentată captivante! Dar te rog, fă omenirii o favoare și nu schimba obiectele vii în iepurași.
