@@ -91,19 +91,180 @@ enum HandGestureState: String, CaseIterable, Identifiable {
9191 }
9292}
9393
94- class HandGestureProcessor {
94+ struct HandGestureUpdate {
95+ let gesture : HandGestureState
96+ let fingerTips : FingerTips ?
97+ let didTransition : Bool
98+ }
99+
100+ final class HandGestureProcessor {
101+ struct Configuration {
102+ let smoothingAlpha : CGFloat
103+ let activationThreshold : CGFloat
104+ let releaseThreshold : CGFloat
105+ let confirmationFrames : Int
106+ let missingFramesBeforeReset : Int
107+
108+ static let balanced = Configuration (
109+ smoothingAlpha: 0.55 ,
110+ activationThreshold: 0.34 ,
111+ releaseThreshold: 0.46 ,
112+ confirmationFrames: 2 ,
113+ missingFramesBeforeReset: 2
114+ )
115+ }
116+
117+ private let configuration : Configuration
118+ private var filteredFrame : HandPoseFrame ?
119+ private var stableGesture : HandGestureState = . none
120+ private var candidateGesture : HandGestureState ?
121+ private var candidateFrameCount = 0
122+ private var missingFrameCount = 0
95123
96- static let threshold : Double = 6
124+ init ( configuration: Configuration = . balanced) {
125+ self . configuration = configuration
126+ }
127+
128+ func process( frame: HandPoseFrame ? ) -> HandGestureUpdate {
129+ guard let frame else {
130+ return processMissingFrame ( )
131+ }
132+
133+ missingFrameCount = 0
134+ let smoothedFrame = smooth ( frame)
135+ filteredFrame = smoothedFrame
136+
137+ let detectedGesture : HandGestureState
138+ if stableGesture != . none,
139+ Self . matches (
140+ stableGesture,
141+ frame: smoothedFrame,
142+ threshold: configuration. releaseThreshold
143+ ) {
144+ detectedGesture = stableGesture
145+ } else {
146+ detectedGesture = Self . classify (
147+ frame: smoothedFrame,
148+ threshold: configuration. activationThreshold
149+ )
150+ }
97151
98- static func process( fingerTips: FingerTips ) -> HandGestureState {
152+ let didTransition = confirm ( detectedGesture)
153+ return HandGestureUpdate (
154+ gesture: stableGesture,
155+ fingerTips: smoothedFrame. fingerTips,
156+ didTransition: didTransition
157+ )
158+ }
159+
160+ func reset( ) {
161+ filteredFrame = nil
162+ stableGesture = . none
163+ candidateGesture = nil
164+ candidateFrameCount = 0
165+ missingFrameCount = 0
166+ }
167+
168+ static func classify(
169+ frame: HandPoseFrame ,
170+ threshold: CGFloat = Configuration . balanced. activationThreshold
171+ ) -> HandGestureState {
99172 for definition in GestureDefinition . priorityOrdered {
100- if definition. matches ( fingerTips : fingerTips , threshold: threshold) {
173+ if definition. matches ( frame : frame , threshold: threshold) {
101174 return definition. state
102175 }
103176 }
104-
105177 return . none
106178 }
179+
180+ private static func matches(
181+ _ gesture: HandGestureState ,
182+ frame: HandPoseFrame ,
183+ threshold: CGFloat
184+ ) -> Bool {
185+ guard let definition = GestureDefinition . priorityOrdered. first ( where: {
186+ $0. state == gesture
187+ } ) else {
188+ return false
189+ }
190+ return definition. matches ( frame: frame, threshold: threshold)
191+ }
192+
193+ private func processMissingFrame( ) -> HandGestureUpdate {
194+ missingFrameCount += 1
195+ guard missingFrameCount >= configuration. missingFramesBeforeReset else {
196+ return HandGestureUpdate (
197+ gesture: stableGesture,
198+ fingerTips: filteredFrame? . fingerTips,
199+ didTransition: false
200+ )
201+ }
202+
203+ let didTransition = stableGesture != . none
204+ filteredFrame = nil
205+ stableGesture = . none
206+ candidateGesture = nil
207+ candidateFrameCount = 0
208+ return HandGestureUpdate (
209+ gesture: . none,
210+ fingerTips: nil ,
211+ didTransition: didTransition
212+ )
213+ }
214+
215+ private func confirm( _ detectedGesture: HandGestureState ) -> Bool {
216+ guard detectedGesture != stableGesture else {
217+ candidateGesture = nil
218+ candidateFrameCount = 0
219+ return false
220+ }
221+
222+ if candidateGesture == detectedGesture {
223+ candidateFrameCount += 1
224+ } else {
225+ candidateGesture = detectedGesture
226+ candidateFrameCount = 1
227+ }
228+
229+ guard candidateFrameCount >= configuration. confirmationFrames else {
230+ return false
231+ }
232+
233+ stableGesture = detectedGesture
234+ candidateGesture = nil
235+ candidateFrameCount = 0
236+ return true
237+ }
238+
239+ private func smooth( _ frame: HandPoseFrame ) -> HandPoseFrame {
240+ guard let previous = filteredFrame else {
241+ return frame
242+ }
243+
244+ return HandPoseFrame (
245+ fingerTips: FingerTips (
246+ thumb: smooth ( previous. fingerTips. thumb, frame. fingerTips. thumb) ,
247+ index: smooth ( previous. fingerTips. index, frame. fingerTips. index) ,
248+ middle: smooth ( previous. fingerTips. middle, frame. fingerTips. middle) ,
249+ ring: smooth ( previous. fingerTips. ring, frame. fingerTips. ring) ,
250+ little: smooth ( previous. fingerTips. little, frame. fingerTips. little)
251+ ) ,
252+ handScale: interpolate ( previous. handScale, frame. handScale) ,
253+ confidence: frame. confidence,
254+ aspectRatio: frame. aspectRatio
255+ )
256+ }
257+
258+ private func smooth( _ previous: CGPoint , _ current: CGPoint ) -> CGPoint {
259+ CGPoint (
260+ x: interpolate ( previous. x, current. x) ,
261+ y: interpolate ( previous. y, current. y)
262+ )
263+ }
264+
265+ private func interpolate( _ previous: CGFloat , _ current: CGFloat ) -> CGFloat {
266+ previous + configuration. smoothingAlpha * ( current - previous)
267+ }
107268}
108269
109270private struct GestureDefinition {
@@ -142,25 +303,36 @@ private struct GestureDefinition {
142303 self . excludedPairs = excludedPairs
143304 }
144305
145- func matches( fingerTips: FingerTips , threshold: Double ) -> Bool {
306+ func matches( frame: HandPoseFrame , threshold: CGFloat ) -> Bool {
307+ let distanceThreshold = frame. handScale * threshold
146308 let requiredPairsMatch = requiredPairs. allSatisfy {
147- distance ( fingerTips: fingerTips, pair: $0) < threshold
309+ distance (
310+ fingerTips: frame. fingerTips,
311+ pair: $0,
312+ aspectRatio: frame. aspectRatio
313+ ) < distanceThreshold
148314 }
149315 let excludedPairsMatch = excludedPairs. allSatisfy {
150- distance ( fingerTips: fingerTips, pair: $0) >= threshold
316+ distance (
317+ fingerTips: frame. fingerTips,
318+ pair: $0,
319+ aspectRatio: frame. aspectRatio
320+ ) >= distanceThreshold
151321 }
152322
153323 return requiredPairsMatch && excludedPairsMatch
154324 }
155325
156326 private func distance(
157327 fingerTips: FingerTips ,
158- pair: ( FingerTipKeyPath , FingerTipKeyPath )
328+ pair: ( FingerTipKeyPath , FingerTipKeyPath ) ,
329+ aspectRatio: CGFloat
159330 ) -> Double {
160- CoordinateHelper . distance (
161- p1: pair. 0 . point ( in: fingerTips) ,
162- p2: pair. 1 . point ( in: fingerTips)
163- )
331+ let p1 = pair. 0 . point ( in: fingerTips)
332+ let p2 = pair. 1 . point ( in: fingerTips)
333+ let dx = ( p1. x - p2. x) * aspectRatio
334+ let dy = p1. y - p2. y
335+ return hypot ( dx, dy)
164336 }
165337}
166338
0 commit comments