This commit is contained in:
Michael Freno
2026-01-31 23:49:06 -05:00
parent a20b3701a6
commit 11f2313b34
3 changed files with 41 additions and 7 deletions

View File

@@ -65,7 +65,8 @@ class EyeTrackingService: NSObject, ObservableObject {
faceWidthScaleMax: 1.4, faceWidthScaleMax: 1.4,
eyeBoundsHorizontalPadding: TrackingConfig.default.eyeBoundsHorizontalPadding, eyeBoundsHorizontalPadding: TrackingConfig.default.eyeBoundsHorizontalPadding,
eyeBoundsVerticalPaddingUp: TrackingConfig.default.eyeBoundsVerticalPaddingUp, eyeBoundsVerticalPaddingUp: TrackingConfig.default.eyeBoundsVerticalPaddingUp,
eyeBoundsVerticalPaddingDown: TrackingConfig.default.eyeBoundsVerticalPaddingDown eyeBoundsVerticalPaddingDown: TrackingConfig.default.eyeBoundsVerticalPaddingDown,
eyeBoundsSmoothing: TrackingConfig.default.eyeBoundsSmoothing
) )
processor.updateConfig(config) processor.updateConfig(config)

View File

@@ -68,7 +68,8 @@ public struct TrackingConfig: Sendable {
faceWidthScaleMax: Double, faceWidthScaleMax: Double,
eyeBoundsHorizontalPadding: Double, eyeBoundsHorizontalPadding: Double,
eyeBoundsVerticalPaddingUp: Double, eyeBoundsVerticalPaddingUp: Double,
eyeBoundsVerticalPaddingDown: Double eyeBoundsVerticalPaddingDown: Double,
eyeBoundsSmoothing: Double
) { ) {
self.horizontalAwayThreshold = horizontalAwayThreshold self.horizontalAwayThreshold = horizontalAwayThreshold
self.verticalAwayThreshold = verticalAwayThreshold self.verticalAwayThreshold = verticalAwayThreshold
@@ -86,6 +87,7 @@ public struct TrackingConfig: Sendable {
self.eyeBoundsHorizontalPadding = eyeBoundsHorizontalPadding self.eyeBoundsHorizontalPadding = eyeBoundsHorizontalPadding
self.eyeBoundsVerticalPaddingUp = eyeBoundsVerticalPaddingUp self.eyeBoundsVerticalPaddingUp = eyeBoundsVerticalPaddingUp
self.eyeBoundsVerticalPaddingDown = eyeBoundsVerticalPaddingDown self.eyeBoundsVerticalPaddingDown = eyeBoundsVerticalPaddingDown
self.eyeBoundsSmoothing = eyeBoundsSmoothing
} }
public let horizontalAwayThreshold: Double public let horizontalAwayThreshold: Double
@@ -104,6 +106,7 @@ public struct TrackingConfig: Sendable {
public let eyeBoundsHorizontalPadding: Double public let eyeBoundsHorizontalPadding: Double
public let eyeBoundsVerticalPaddingUp: Double public let eyeBoundsVerticalPaddingUp: Double
public let eyeBoundsVerticalPaddingDown: Double public let eyeBoundsVerticalPaddingDown: Double
public let eyeBoundsSmoothing: Double
public static let `default` = TrackingConfig( public static let `default` = TrackingConfig(
horizontalAwayThreshold: 0.08, horizontalAwayThreshold: 0.08,
@@ -121,6 +124,7 @@ public struct TrackingConfig: Sendable {
faceWidthScaleMax: 1.4, faceWidthScaleMax: 1.4,
eyeBoundsHorizontalPadding: 0.1, eyeBoundsHorizontalPadding: 0.1,
eyeBoundsVerticalPaddingUp: 0.9, eyeBoundsVerticalPaddingUp: 0.9,
eyeBoundsVerticalPaddingDown: 0.4 eyeBoundsVerticalPaddingDown: 0.4,
eyeBoundsSmoothing: 0.2
) )
} }

View File

@@ -32,6 +32,8 @@ final class VisionGazeProcessor: @unchecked Sendable {
private let baselineModel = GazeBaselineModel() private let baselineModel = GazeBaselineModel()
private var faceWidthBaseline: Double? private var faceWidthBaseline: Double?
private var faceWidthSmoothed: Double? private var faceWidthSmoothed: Double?
private var leftEyeFrameSmoothed: CGRect?
private var rightEyeFrameSmoothed: CGRect?
private var config: TrackingConfig private var config: TrackingConfig
init(config: TrackingConfig) { init(config: TrackingConfig) {
@@ -46,6 +48,8 @@ final class VisionGazeProcessor: @unchecked Sendable {
baselineModel.reset() baselineModel.reset()
faceWidthBaseline = nil faceWidthBaseline = nil
faceWidthSmoothed = nil faceWidthSmoothed = nil
leftEyeFrameSmoothed = nil
rightEyeFrameSmoothed = nil
} }
func process(analysis: VisionPipeline.FaceAnalysis) -> ObservationResult { func process(analysis: VisionPipeline.FaceAnalysis) -> ObservationResult {
@@ -77,13 +81,15 @@ final class VisionGazeProcessor: @unchecked Sendable {
eye: landmarks.leftEye, eye: landmarks.leftEye,
pupil: landmarks.leftPupil, pupil: landmarks.leftPupil,
face: face, face: face,
imageSize: analysis.imageSize imageSize: analysis.imageSize,
smoothingRect: &leftEyeFrameSmoothed
) )
let rightEye = makeEyeObservation( let rightEye = makeEyeObservation(
eye: landmarks.rightEye, eye: landmarks.rightEye,
pupil: landmarks.rightPupil, pupil: landmarks.rightPupil,
face: face, face: face,
imageSize: analysis.imageSize imageSize: analysis.imageSize,
smoothingRect: &rightEyeFrameSmoothed
) )
let eyesClosed = detectEyesClosed(left: leftEye, right: rightEye) let eyesClosed = detectEyesClosed(left: leftEye, right: rightEye)
@@ -126,7 +132,8 @@ final class VisionGazeProcessor: @unchecked Sendable {
eye: VNFaceLandmarkRegion2D?, eye: VNFaceLandmarkRegion2D?,
pupil: VNFaceLandmarkRegion2D?, pupil: VNFaceLandmarkRegion2D?,
face: VNFaceObservation, face: VNFaceObservation,
imageSize: CGSize imageSize: CGSize,
smoothingRect: inout CGRect?
) -> EyeObservation? { ) -> EyeObservation? {
guard let eye else { return nil } guard let eye else { return nil }
@@ -142,8 +149,10 @@ final class VisionGazeProcessor: @unchecked Sendable {
pupilPoint = bounds.center pupilPoint = bounds.center
} }
let rawFrame = CGRect(x: bounds.minX, y: bounds.minY, width: bounds.size.width, height: bounds.size.height)
let smoothedFrame = smoothRect(rawFrame, existing: &smoothingRect, smoothing: config.eyeBoundsSmoothing)
let paddedFrame = expandRect( let paddedFrame = expandRect(
CGRect(x: bounds.minX, y: bounds.minY, width: bounds.size.width, height: bounds.size.height), smoothedFrame,
horizontalPadding: config.eyeBoundsHorizontalPadding, horizontalPadding: config.eyeBoundsHorizontalPadding,
verticalPaddingUp: config.eyeBoundsVerticalPaddingUp, verticalPaddingUp: config.eyeBoundsVerticalPaddingUp,
verticalPaddingDown: config.eyeBoundsVerticalPaddingDown verticalPaddingDown: config.eyeBoundsVerticalPaddingDown
@@ -238,6 +247,26 @@ final class VisionGazeProcessor: @unchecked Sendable {
) )
} }
private func smoothRect(_ rect: CGRect, existing: inout CGRect?, smoothing: Double) -> CGRect {
guard smoothing > 0, smoothing < 1 else {
existing = rect
return rect
}
if let current = existing {
let newOriginX = current.origin.x + (rect.origin.x - current.origin.x) * smoothing
let newOriginY = current.origin.y + (rect.origin.y - current.origin.y) * smoothing
let newWidth = current.size.width + (rect.size.width - current.size.width) * smoothing
let newHeight = current.size.height + (rect.size.height - current.size.height) * smoothing
let updated = CGRect(x: newOriginX, y: newOriginY, width: newWidth, height: newHeight)
existing = updated
return updated
}
existing = rect
return rect
}
private func averageCoordinate(left: CGFloat?, right: CGFloat?, fallback: Double?) -> Double? { private func averageCoordinate(left: CGFloat?, right: CGFloat?, fallback: Double?) -> Double? {
switch (left, right) { switch (left, right) {
case let (left?, right?): case let (left?, right?):