feat(ios): vision hands and face over usb
This commit is contained in:
@@ -4,6 +4,7 @@ import Combine
|
||||
import Foundation
|
||||
import RealityKit
|
||||
import SwiftUI
|
||||
import Vision
|
||||
|
||||
/// Drives the ARKit body-tracking session and streams the 91-joint
|
||||
/// skeleton plus HEVC-encoded camera video to the tethered Mac over
|
||||
@@ -37,6 +38,10 @@ final class ARBodySession: NSObject, ObservableObject, ARSessionDelegate {
|
||||
private let videoEncoder = VideoEncoder()
|
||||
private var videoStarted = false
|
||||
private var lastFrameTime: TimeInterval = 0
|
||||
private let visionQueue = DispatchQueue(
|
||||
label: "cc.saillant.arbody.vision", qos: .userInitiated)
|
||||
private var visionBusy = false
|
||||
private var lastVisionTime: TimeInterval = 0
|
||||
private var jointsInSecond: Int = 0
|
||||
private var lastSecond: TimeInterval = 0
|
||||
private let bodyParents: [Int] =
|
||||
@@ -126,6 +131,17 @@ final class ARBodySession: NSObject, ObservableObject, ARSessionDelegate {
|
||||
self.videoEncoder.encode(img, pts: t)
|
||||
}
|
||||
|
||||
// Vision hands+face at ~15 fps, off-main, drop if busy.
|
||||
if t - self.lastVisionTime >= 1.0 / 15.0, !self.visionBusy {
|
||||
self.lastVisionTime = t
|
||||
self.visionBusy = true
|
||||
let buf = img // CVPixelBuffer captured for the worker
|
||||
self.visionQueue.async {
|
||||
self.runVision(on: buf, timestamp: t)
|
||||
Task { @MainActor in self.visionBusy = false }
|
||||
}
|
||||
}
|
||||
|
||||
var count: Int = 0
|
||||
var firstBody: ARBodyAnchor?
|
||||
for anchor in frame.anchors {
|
||||
@@ -187,6 +203,73 @@ final class ARBodySession: NSObject, ObservableObject, ARSessionDelegate {
|
||||
/// indices without re-reading the ARKit skeleton definition.
|
||||
var bodyParentIndices: [Int] { bodyParents }
|
||||
|
||||
// MARK: - Vision hand + face capture
|
||||
|
||||
nonisolated func runVision(on pixelBuffer: CVPixelBuffer,
|
||||
timestamp t: TimeInterval) {
|
||||
let handReq = VNDetectHumanHandPoseRequest()
|
||||
handReq.maximumHandCount = 2
|
||||
let faceReq = VNDetectFaceLandmarksRequest()
|
||||
let handler = VNImageRequestHandler(
|
||||
cvPixelBuffer: pixelBuffer, orientation: .right, options: [:])
|
||||
try? handler.perform([handReq, faceReq])
|
||||
|
||||
// ---- hands ----
|
||||
// Fixed 21-joint order matching MediaPipe-style indexing.
|
||||
let order: [VNHumanHandPoseObservation.JointName] = [
|
||||
.wrist,
|
||||
.thumbCMC, .thumbMP, .thumbIP, .thumbTip,
|
||||
.indexMCP, .indexPIP, .indexDIP, .indexTip,
|
||||
.middleMCP, .middlePIP, .middleDIP, .middleTip,
|
||||
.ringMCP, .ringPIP, .ringDIP, .ringTip,
|
||||
.littleMCP, .littlePIP, .littleDIP, .littleTip,
|
||||
]
|
||||
var handsOut: [HandsPayload.Hand] = []
|
||||
for obs in (handReq.results ?? []).prefix(2) {
|
||||
guard let pts = try? obs.recognizedPoints(.all) else { continue }
|
||||
let p = order.map { name -> SIMD3<Float> in
|
||||
if let rp = pts[name] {
|
||||
// Vision: normalized, bottom-left origin -> flip y.
|
||||
return SIMD3(Float(rp.location.x),
|
||||
Float(1.0 - rp.location.y),
|
||||
Float(rp.confidence))
|
||||
}
|
||||
return .zero
|
||||
}
|
||||
handsOut.append(HandsPayload.Hand(
|
||||
isRight: obs.chirality == .right, points: p))
|
||||
}
|
||||
|
||||
// ---- face (most prominent) ----
|
||||
var facePayload: FacePayload?
|
||||
if let face = (faceReq.results ?? []).first,
|
||||
let all = face.landmarks?.allPoints {
|
||||
let bb = face.boundingBox
|
||||
let pts = all.normalizedPoints.map { np -> SIMD2<Float> in
|
||||
// landmark points are relative to the face bbox.
|
||||
let x = bb.origin.x + Double(np.x) * bb.size.width
|
||||
let y = bb.origin.y + Double(np.y) * bb.size.height
|
||||
return SIMD2(Float(x), Float(1.0 - y))
|
||||
}
|
||||
facePayload = FacePayload(
|
||||
confidence: face.confidence, points: pts)
|
||||
}
|
||||
|
||||
let handsData = HandsPayload(hands: handsOut).encoded()
|
||||
let faceData = facePayload?.encoded()
|
||||
Task { @MainActor in
|
||||
guard self.usbState == .connected else { return }
|
||||
self.usb.send(tag: .hands, pid: -1,
|
||||
timestamp: t, payload: handsData)
|
||||
if let faceData {
|
||||
self.usb.send(tag: .face, pid: -1,
|
||||
timestamp: t, payload: faceData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - USB skeleton
|
||||
|
||||
private func publishUSB(pid: Int, timestamp: TimeInterval,
|
||||
body: ARBodyAnchor) {
|
||||
guard usbState == .connected else { return }
|
||||
|
||||
Reference in New Issue
Block a user