Real-time hand pose detection for iOS using Apple's Vision framework. Detects Point, Peace, Pinch, and Hand poses with angle-based joint analysis, window-based temporal smoothing, and a protocol-oriented AVFoundation/Vision pipeline.
Live demo of real-time hand pose detection with skeleton overlay and smoothed pose labels:
Hand-Point-Detection-Demo-Video.mov
- Real-time hand skeleton visualization with 21 joints
- Angle-based pose recognition (Point, Peace, Pinch, Hand)
- Window-based smoothing to reduce flickering between poses
- Protocol-oriented AVFoundation + Vision pipeline
- AsyncStream for continuous frame processing
- Live camera preview with hand skeleton overlay
- Classified pose label (Point, Peace, Pinch, Hand)
- Dedicated states for no hand detected, incomplete hand, and camera errors
- Clone the repository:
git clone https://github.com/YvonneG-Dev/HandPoseDetection.git- Open the project in Xcode
- Run on a physical device (front camera required)
The app follows MVVM, with a protocol-oriented pipeline for both vision capture and pose classification.
VisionViewModel(@Observable) exposes a singlehandPoseStatethat drives a fully declarativePoseDetectionView, and is constructed via dependency injection for testabilityAVFoundationCameraControllerandAppleVisionHandPointDetectorimplement theCameraControllerandHandPointDetectorprotocols and communicate via delegate protocols, matching AVFoundation/Vision's native callback styleVisionPipeline(protocol) bridges these delegate callbacks into a singleAsyncStream<HandJointDetectionResult>, so everything above it only deals withasync/awaitPoseClassificationPipeline(protocol) coordinates aPoseDetectorand aPoseSmootherin sequence, decoupling gesture classification from temporal stabilizationGeometryBasedPoseDetectorclassifies gestures viaatan2-based joint angles — deterministic and unit-testable with no camera or Vision request involvedWindowBasedPoseSmootherstabilizes the classified pose over a sliding window of recent frames, falling back to the last known joint positions when the current frame lacks them- Dependency injection throughout — every layer (
VisionPipeline,PoseClassificationPipeline,VisionViewModel) is constructed via a designated initializer accepting protocol types, with a convenience initializer wiring up production defaults - Errors as stream values — camera and detection failures are modeled as result cases rather than thrown errors, since an
AsyncThrowingStreamwould terminate the pipeline on the first bad frame
flowchart TD
subgraph VisionGroup["Vision Capture"]
CC["CameraController (Protocol)<br/><i>AVFoundationCameraController</i>"]
HPD["HandPointDetector (Protocol)<br/><i>AppleVisionHandPointDetector</i>"]
VP["VisionPipeline (Protocol)<br/><i>CameraVisionPipeline</i>"]
CC --> VP
HPD --> VP
end
subgraph ClassificationGroup["Pose Classification"]
PCP["PoseClassificationPipeline (Protocol)<br/><i>SequentialPoseClassificationPipeline</i>"]
PD["PoseDetector (Protocol)<br/><i>GeometryBasedPoseDetector</i>"]
PS["PoseSmoother (Protocol)<br/><i>WindowBasedPoseSmoother</i>"]
PCP --> PD
PCP --> PS
end
VP -- "AsyncStream" --> VVM
VVM --> PCP
PCP -- "PoseState" --> VVM
VVM["VisionViewModel"] ---> PDV["PoseDetectionView"]
style VisionGroup fill:#e1f5fe
style ClassificationGroup fill:#e8f5e9
style VVM fill:#fce4ec
style PDV fill:#fff3e0
The project includes unit tests for the core domain logic:
GeometryBasedPoseDetectorTest— angle-based classification, including confidence thresholds and degenerate bounding boxesWindowBasedPoseSmootherTest— window stability, ambiguous windows, and joint continuity through outlier framesBoundingBoxTest— geometry edge cases, including degenerate and negative dimensionsHandJointTest— the non-failable initializer, the failable initializer's nil case, bounding box construction from joint positions, and finger chain structureVisionViewModelTest— verifiesVisionViewModelcorrectly wires the vision stream to pose classification, using mockedVisionPipelineandPoseClassificationPipeline
All dependencies are injected behind protocols, so classification, smoothing, and the ViewModel can each be tested without a camera or Vision request.
The app tracks a single hand (maximumHandCount = 1) and targets iPhone in portrait
orientation only. Pose classification is smoothed over a sliding window. Hand is a
fallback state for a detected hand that matches none of the specific gestures, rather
than a positively classified pose — the generic .hand fallback also applies no
confidence threshold, as confidence is only relevant to specific gesture classifications
rather than hand presence itself.
- iOS 17+
- Xcode 15+
- Physical device (front camera required)
This is a personal portfolio project. Pull requests will not be accepted, but feedback and suggestions via Issues are welcome.