VITUREカメラとmacOSでハンドトラッキング

 Unity系と各OS向けのSDKはあるのだけど、あんまり存在意義がわからない。センサ系のアタッチに必要なのかな。表示オブジェクトのヘッドトラッキングとかができる模様。

VITURE: Next Gen XR Glasses | VITURE
https://www.viture.com/developer

 SDK以外にも、SpaceWalkerっていう、VITURE公式のXRグラス用アプリ環境があるみたい。

 macOS版では、VITUREグラスを単なる外部ディスプレイとして使うだけでなく、たとえば以下ができるらしい。

 ・頭の動きに合わせた 3DoFヘッドトラッキング

 ・仮想的な マルチディスプレイ

 ・横長の ウルトラワイド画面

 ・仮想画面を空間内に固定するような表示

 ・2D映像の3D変換

 ダウンロードは、ここからできる。

SpaceWalker & VITURE AR/XR Glasses Apps | Download Center | VITURE
https://www.viture.com/academy/download

 なんだ、アイコンに通行止めマークついてる。

 あー、OSが対応できない古さの悲しみ。まぁ、古過ぎるmacOS 11.6.1使っているのが悪いよね。

 とりあえず、UVCカメラは認識しているので、見えているmacOSのデスクトップを操作するのに、カメラからのゼスチャ認識をやりたい。

 Glasses SDKを使わずに、macOSでハンドトラッキングするには、visionOSで追加されたVNDetectHumanHandPoseRequestクラスがある。これはmacOS 11.0以降対応なので、これを使おう。今はぜんぜん使ってないけど、いっぱいあるIntel Macでも古いOSでも動くしね。

VNDetectHumanHandPoseRequest | Apple Developer Documentation
https://developer.apple.com/documentation/vision/vndetecthumanhandposerequest

 カメラが見えているか確認。

% system_profiler SPCameraDataType
2026-09-05 22:07:15.782 system_profiler[7719:13799749] Error loading /Library/CoreMediaIO/Plug-Ins/DAL/obs-mac-virtualcam.plugin/Contents/MacOS/obs-mac-virtualcam:  dlopen(/Library/CoreMediaIO/Plug-Ins/DAL/obs-mac-virtualcam.plugin/Contents/MacOS/obs-mac-virtualcam, 0x0106): missing compatible arch in /Library/CoreMediaIO/Plug-Ins/DAL/obs-mac-virtualcam.plugin/Contents/MacOS/obs-mac-virtualcam
Camera:

USB 2.0 Camera:

Model ID: UVC Camera VendorID_3141 ProductID_25451
Unique ID: 0x11200000c45636b

FaceTime HD Camera:

Model ID: Apple Camera VendorID_0x106B ProductID_0x000B
Unique ID: EAB7A68FEC2B4487AADFD8A91C1CB782


% ffmpeg -f avfoundation -list_devices true -i ""

ffmpeg version 6.0 Copyright (c) 2000-2023 the FFmpeg developers
built with Apple clang version 13.0.0 (clang-1300.0.29.30)
configuration: --prefix=/opt/homebrew/Cellar/ffmpeg/6.0 --enable-shared --enable-pthreads --enable-version3 --cc=clang --host-cflags= --host-ldflags= --enable-ffplay --enable-gnutls --enable-gpl --enable-libaom --enable-libaribb24 --enable-libbluray --enable-libdav1d --enable-libmp3lame --enable-libopus --enable-librav1e --enable-librist --enable-librubberband --enable-libsnappy --enable-libsrt --enable-libsvtav1 --enable-libtesseract --enable-libtheora --enable-libvidstab --enable-libvmaf --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libxvid --enable-lzma --enable-libfontconfig --enable-libfreetype --enable-frei0r --enable-libass --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libspeex --enable-libsoxr --enable-libzmq --enable-libzimg --disable-libjack --disable-indev=jack --enable-videotoolbox --enable-audiotoolbox --enable-neon
libavutil      58.  2.100 / 58.  2.100
libavcodec     60.  3.100 / 60.  3.100
libavformat    60.  3.100 / 60.  3.100
libavdevice    60.  1.100 / 60.  1.100
libavfilter     9.  3.100 /  9.  3.100
libswscale      7.  1.100 /  7.  1.100
libswresample   4. 10.100 /  4. 10.100
libpostproc    57.  1.100 / 57.  1.100
[AVFoundation indev @ 0x122f0edc0] AVFoundation video devices:
[AVFoundation indev @ 0x122f0edc0] [0] OBS Virtual Camera
[AVFoundation indev @ 0x122f0edc0] [1] USB 2.0 Camera
[AVFoundation indev @ 0x122f0edc0] [2] FaceTime HD Camera
[AVFoundation indev @ 0x122f0edc0] [3] Capture screen 0
[AVFoundation indev @ 0x122f0edc0] [4] Capture screen 1
[AVFoundation indev @ 0x122f0edc0] AVFoundation audio devices:
[AVFoundation indev @ 0x122f0edc0] [0] VITURE Microphone
[AVFoundation indev @ 0x122f0edc0] [1] 機器セット
[AVFoundation indev @ 0x122f0edc0] [2] BlackHole 2ch
[AVFoundation indev @ 0x122f0edc0] [3] MacBook Proのマイク
[AVFoundation indev @ 0x122f0edc0] [4] Microsoft Teams Audio
: Input/output error

 最後に出ているエラーは、「-list_devices true -i ""」でデバイス一覧だけを取得したために出るもので、異常ではない。

 まず画面に手の認識状態を出すSwift製の最小CLIプログラムを作ってみる。こんな感じで出ればいいかな。手があるかと、場所と、指確認と、つまんでいるかのチェック。

Hand detected
index:  x=0.42 y=0.71
thumb:  x=0.39 y=0.68
pinch:  YES

 こんな感じ。

% cd Downloads/viture_hand_probe
% cat build.sh
#!/bin/zsh
set -eu

swiftc main.swift \
-o viture-hand-probe \
-framework AVFoundation \
-framework Vision \
-framework CoreMedia \
-framework CoreVideo \
-Xlinker -sectcreate \
-Xlinker __TEXT \
-Xlinker __info_plist \
-Xlinker Info.plist

echo "built: ./viture-hand-probe"

 % cat main.swift

import Foundation
import AVFoundation
import Vision
import CoreMedia

final class HandPoseDetector: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
func captureOutput(_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection) {
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
return
}

// This delegate runs on a dedicated serial queue. Vision is intentionally
// performed synchronously here; alwaysDiscardsLateVideoFrames prevents a
// backlog when hand-pose inference is slower than the camera frame rate.
let request = VNDetectHumanHandPoseRequest()
request.maximumHandCount = 1

let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer,
orientation: .up,
options: [:])
do {
try handler.perform([request])
} catch {
fputs("Vision error: \(error)\n", stderr)
return
}

guard let observation = request.results?.first else {
print("no hand")
return
}

do {
let thumb = try observation.recognizedPoint(.thumbTip)
let index = try observation.recognizedPoint(.indexTip)
let middle = try observation.recognizedPoint(.middleTip)
let wrist = try observation.recognizedPoint(.wrist)
let indexMCP = try observation.recognizedPoint(.indexMCP)
let pinkyMCP = try observation.recognizedPoint(.littleMCP)

let minConfidence: VNConfidence = 0.25
guard thumb.confidence >= minConfidence,
index.confidence >= minConfidence,
wrist.confidence >= minConfidence,
indexMCP.confidence >= minConfidence,
pinkyMCP.confidence >= minConfidence else {
print("hand detected, but key points are low-confidence")
return
}

let pinchDistance = hypot(index.location.x - thumb.location.x,
index.location.y - thumb.location.y)

// Normalize by palm width so pinch threshold is less sensitive
// to how close the hand is to the camera.
let palmWidth = max(
hypot(indexMCP.location.x - pinkyMCP.location.x,
indexMCP.location.y - pinkyMCP.location.y),
0.001
)
let pinchRatio = pinchDistance / palmWidth
let pinching = pinchRatio < 0.45

let middleText: String
if middle.confidence >= minConfidence {
middleText = String(format: " middle=(%.3f, %.3f)",
middle.location.x, middle.location.y)
} else {
middleText = ""
}

print(String(format:
"index=(%.3f, %.3f) thumb=(%.3f, %.3f)%@ pinchRatio=%.3f pinch=%@",
index.location.x, index.location.y,
thumb.location.x, thumb.location.y,
middleText,
pinchRatio,
pinching ? "YES" : "NO"
))
} catch {
print("point extraction error: \(error)")
}
}
}

func requestCameraAccess() -> Bool {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
return true
case .notDetermined:
let sem = DispatchSemaphore(value: 0)
var allowed = false
AVCaptureDevice.requestAccess(for: .video) { granted in
allowed = granted
sem.signal()
}
sem.wait()
return allowed
case .denied, .restricted:
return false
@unknown default:
return false
}
}

func findVitureCamera() -> AVCaptureDevice? {
let discovery = AVCaptureDevice.DiscoverySession(
deviceTypes: [.externalUnknown, .builtInWideAngleCamera],
mediaType: .video,
position: .unspecified
)

// Prefer the name currently reported by macOS for the Luma Ultra camera.
if let exact = discovery.devices.first(where: { $0.localizedName == "USB 2.0 Camera" }) {
return exact
}

// Fallback: any external camera before the built-in FaceTime camera.
if let external = discovery.devices.first(where: {
$0.localizedName.localizedCaseInsensitiveContains("USB") ||
!$0.localizedName.localizedCaseInsensitiveContains("FaceTime")
}) {
return external
}

return nil
}

guard requestCameraAccess() else {
fputs("Camera access denied. Enable camera access for Terminal (or the app launching this executable) in System Preferences > Security & Privacy > Privacy > Camera.\n", stderr)
exit(1)
}

guard let camera = findVitureCamera() else {
fputs("USB 2.0 Camera was not found.\n", stderr)
exit(2)
}

print("Using camera: \(camera.localizedName)")

let session = AVCaptureSession()
session.beginConfiguration()

if session.canSetSessionPreset(.hd1280x720) {
session.sessionPreset = .hd1280x720
}

let input: AVCaptureDeviceInput
do {
input = try AVCaptureDeviceInput(device: camera)
} catch {
fputs("Failed to create camera input: \(error)\n", stderr)
exit(3)
}

guard session.canAddInput(input) else {
fputs("Cannot add camera input.\n", stderr)
exit(4)
}
session.addInput(input)

let output = AVCaptureVideoDataOutput()
output.alwaysDiscardsLateVideoFrames = true
output.videoSettings = [
kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)
]

let detector = HandPoseDetector()
let captureQueue = DispatchQueue(label: "viture.handpose.capture")
output.setSampleBufferDelegate(detector, queue: captureQueue)

guard session.canAddOutput(output) else {
fputs("Cannot add video output.\n", stderr)
exit(5)
}
session.addOutput(output)
session.commitConfiguration()

session.startRunning()
print("Hand tracking started. Press Ctrl-C to stop.")

RunLoop.main.run()

 ビルドする。

% ./build.sh
built: ./viture-hand-probe

 実行してみる。

 % ./viture-hand-probe

 デバイスへのアクセス許可が出る。

 ログがダラダラと流れてくるので、カメラの視野に入るようにして、画角の中に手を入れたり、ひらひらさせたり、指ひろげたり、すぼめたり、二本でピンチしたりしてみる。

% ./viture-hand-probe
Using camera: USB 2.0 Camera
Hand tracking started. Press Ctrl-C to stop.
no hand
index=(0.550, 0.840) thumb=(0.883, 0.744) middle=(0.454, 0.710) pinchRatio=2.546 pinch=NO
index=(0.668, 0.774) thumb=(0.772, 0.684) pinchRatio=0.400 pinch=YES
index=(0.920, 0.610) thumb=(0.911, 0.789) middle=(0.841, 0.439) pinchRatio=0.436 pinch=YES
hand detected, but key points are low-confidence
index=(0.522, 0.335) thumb=(0.554, 0.393) pinchRatio=0.243 pinch=YES
no hand
no hand
index=(0.802, 0.124) thumb=(0.798, 0.202) middle=(0.844, 0.166) pinchRatio=1.349 pinch=NO

 一応とれるけど、精度がイマイチだな。

 一応動くのだけど、結果をもとに操作感のチューニングをやってみるけれど、とても難しい...

 macのトラックパッドと同じように、人差し指でカーソル移動、二本指並んでいるとスクロール、親指と人差し指でタップ、ダブルタップを割り当ててみる。

 ・人差し指1本 → カーソル移動

 ・人差し指+中指を並べる → 2本指スクロール

 ・親指+人差し指を短くピンチして離す → タップ=左クリック

 ・短いピンチを2回 → ダブルタップ=ダブルクリック

 ・親指+人差し指を一定時間閉じたまま移動 → ドラッグ

 やってみたけど、手を持ち上げてずっと操作するのは、かなり面倒なので、他の方法を考えないといけない。これは、VRとかARのときと同じ結論だね。わかってたけどやってみただけw

 やっぱり、なんか手に持つ操作デバイスがいるよな。なんか手配するか。

 画面を表示しながら一定時間たつと、過熱防止のためか画面が切れるのね。ボタン押すと復活する。


オリジナル投稿:
VITUREカメラとmacOSでハンドトラッキング|kinneko|pixivFANBOX
https://kinneko.fanbox.cc/posts/12572077