Skip to content

Commit ba5e1ae

Browse files
authored
chore: update face bounding box landmark calculations (#77)
* chore: update face bounding box landmark calculations * fix bounding box width and height * fix typo on bounding box right
1 parent 471ac56 commit ba5e1ae

File tree

5 files changed

+197
-38
lines changed

5 files changed

+197
-38
lines changed

Sources/FaceLiveness/FaceDetection/BlazeFace/DetectedFace.swift

Lines changed: 62 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,53 +14,79 @@ struct DetectedFace {
1414
let rightEye: CGPoint
1515
let nose: CGPoint
1616
let mouth: CGPoint
17+
let rightEar: CGPoint
18+
let leftEar: CGPoint
1719

1820
let confidence: Float
1921

20-
func boundingBoxFromLandmarks() -> CGRect {
21-
let eyeCenterX = (leftEye.x + rightEye.x) / 2
22-
let eyeCenterY = (leftEye.y + rightEye.y) / 2
23-
24-
let cx = (nose.x + eyeCenterX) / 2
25-
let cy = (nose.y + eyeCenterY) / 2
26-
27-
let ow = sqrt(pow((leftEye.x - rightEye.x), 2) + pow((leftEye.y - rightEye.y), 2)) * 2
28-
let oh = 1.618 * ow
29-
let minX = cx - ow / 2
30-
let minY = cy - oh / 2
31-
32-
let rect = CGRect(x: minX, y: minY, width: ow, height: oh)
22+
func boundingBoxFromLandmarks(ovalRect: CGRect) -> CGRect {
23+
let alpha = 2.0
24+
let gamma = 1.8
25+
let ow = (alpha * pupilDistance + gamma * faceHeight) / 2
26+
var cx = (eyeCenterX + nose.x) / 2
27+
28+
if ovalRect != CGRect.zero {
29+
let ovalTop = ovalRect.minY
30+
let ovalHeight = ovalRect.maxY - ovalRect.minY
31+
if eyeCenterY > (ovalTop + ovalHeight) / 2 {
32+
cx = eyeCenterX
33+
}
34+
}
35+
36+
let faceWidth = ow
37+
let faceHeight = 1.68 * faceWidth
38+
let faceBoxBottom = boundingBox.maxY
39+
let faceBoxTop = faceBoxBottom - faceHeight
40+
let faceBoxLeft = min(cx - ow / 2, rightEar.x)
41+
let faceBoxRight = max(cx + ow / 2, leftEar.x)
42+
let width = faceBoxRight - faceBoxLeft
43+
let height = faceBoxBottom - faceBoxTop
44+
let rect = CGRect(x: faceBoxLeft, y: faceBoxTop, width: width, height: height)
3345
return rect
3446
}
3547

3648
var faceDistance: CGFloat {
3749
sqrt(pow(rightEye.x - leftEye.x, 2) + pow(rightEye.y - leftEye.y, 2))
3850
}
51+
52+
var pupilDistance: CGFloat {
53+
sqrt(pow(leftEye.x - rightEye.x, 2) + pow(leftEye.y - rightEye.y, 2))
54+
}
55+
56+
var eyeCenterX: CGFloat {
57+
(leftEye.x + rightEye.x) / 2
58+
}
59+
60+
var eyeCenterY: CGFloat {
61+
(leftEye.y + rightEye.y) / 2
62+
}
63+
64+
var faceHeight: CGFloat {
65+
sqrt(pow(eyeCenterX - mouth.x, 2) + pow(eyeCenterY - mouth.y, 2))
66+
}
3967

4068
func normalize(width: CGFloat, height: CGFloat) -> DetectedFace {
41-
.init(
42-
boundingBox: .init(
43-
x: boundingBox.minX * width,
44-
y: boundingBox.minY * height,
45-
width: boundingBox.width * width,
46-
height: boundingBox.height * height
47-
),
48-
leftEye: .init(
49-
x: leftEye.x * width,
50-
y: leftEye.y * height
51-
),
52-
rightEye: .init(
53-
x: rightEye.x * width,
54-
y: rightEye.y * height
55-
),
56-
nose: .init(
57-
x: nose.x * width,
58-
y: nose.y * height
59-
),
60-
mouth: .init(
61-
x: mouth.x * width,
62-
y: mouth.y * height
63-
),
69+
let boundingBox = CGRect(
70+
x: boundingBox.minX * width,
71+
y: boundingBox.minY * height,
72+
width: boundingBox.width * width,
73+
height: boundingBox.height * height
74+
)
75+
let leftEye = CGPoint(x: leftEye.x * width, y: leftEye.y * height)
76+
let rightEye = CGPoint(x: rightEye.x * width, y: rightEye.y * height)
77+
let nose = CGPoint(x: nose.x * width, y: nose.y * height)
78+
let mouth = CGPoint(x: mouth.x * width, y: mouth.y * height)
79+
let rightEar = CGPoint(x: rightEar.x * width, y: rightEar.y * height)
80+
let leftEar = CGPoint(x: leftEar.x * width, y: leftEar.y * height)
81+
82+
return DetectedFace(
83+
boundingBox: boundingBox,
84+
leftEye: leftEye,
85+
rightEye: rightEye,
86+
nose: nose,
87+
mouth: mouth,
88+
rightEar: rightEar,
89+
leftEar: leftEar,
6490
confidence: confidence
6591
)
6692
}

Sources/FaceLiveness/FaceDetection/BlazeFace/FaceDetectorShortRange+Model.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ extension FaceDetectorShortRange {
157157
let leftEye = faceResult[3]
158158
let nose = faceResult[4]
159159
let mouth = faceResult[5]
160+
let rightEar = faceResult[6]
161+
let leftEar = faceResult[7]
162+
160163

161164

162165
let boundingBox = CGRect(
@@ -172,6 +175,8 @@ extension FaceDetectorShortRange {
172175
rightEye: .init(x: rightEye.x, y: rightEye.y),
173176
nose: .init(x: nose.x, y: nose.y),
174177
mouth: .init(x: mouth.x, y: mouth.y),
178+
rightEar: .init(x: rightEar.x, y: rightEar.y),
179+
leftEar: .init(x: leftEar.x, y: leftEar.y),
175180
confidence: overlappingConfidenceScore / Float(overlappingOutputs.count)
176181
)
177182

Sources/FaceLiveness/Views/Liveness/FaceLivenessDetectionViewModel+FaceDetectionResultHandler.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ extension FaceLivenessDetectionViewModel: FaceDetectionResultHandler {
2828
}
2929
case .singleFace(let face):
3030
var normalizedFace = normalizeFace(face)
31-
normalizedFace.boundingBox = normalizedFace.boundingBoxFromLandmarks()
31+
normalizedFace.boundingBox = normalizedFace.boundingBoxFromLandmarks(ovalRect: ovalRect)
3232

3333
switch livenessState.state {
3434
case .pendingFacePreparedConfirmation:
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import XCTest
9+
@testable import FaceLiveness
10+
11+
12+
final class DetectedFaceTests: XCTestCase {
13+
var detectedFace: DetectedFace!
14+
var expectedNormalizeFace: DetectedFace!
15+
let normalizeWidth = 414.0
16+
let normalizeHeight = 552.0
17+
18+
override func setUp() {
19+
let boundingBox = CGRect(
20+
x: 0.15805082494171963,
21+
y: 0.3962942063808441,
22+
width: 0.6549023386310235,
23+
height: 0.49117204546928406
24+
)
25+
let leftEye = CGPoint(x: 0.6686329891870315, y: 0.48738187551498413)
26+
let rightEye = CGPoint(x: 0.35714725227596134, y: 0.4664449691772461)
27+
let nose = CGPoint(x: 0.5283648181467697, y: 0.5319401621818542)
28+
let mouth = CGPoint(x: 0.5062596005080024, y: 0.689265251159668)
29+
let rightEar = CGPoint(x: 0.1658528943614037, y: 0.5668278932571411)
30+
let leftEar = CGPoint(x: 0.7898947484263203, y: 0.5973731875419617)
31+
let confidence: Float = 0.94027895
32+
detectedFace = DetectedFace(
33+
boundingBox: boundingBox,
34+
leftEye: leftEye,
35+
rightEye: rightEye,
36+
nose: nose,
37+
mouth: mouth,
38+
rightEar: rightEar,
39+
leftEar: leftEar,
40+
confidence: confidence
41+
)
42+
43+
let normalizedBoundingBox = CGRect(
44+
x: 0.15805082494171963 * normalizeWidth,
45+
y: 0.3962942063808441 * normalizeHeight,
46+
width: 0.6549023386310235 * normalizeWidth,
47+
height: 0.49117204546928406 * normalizeHeight
48+
)
49+
let normalizedLeftEye = CGPoint(
50+
x: 0.6686329891870315 * normalizeWidth,
51+
y: 0.48738187551498413 * normalizeHeight
52+
)
53+
let normalizedRightEye = CGPoint(
54+
x: 0.35714725227596134 * normalizeWidth,
55+
y: 0.4664449691772461 * normalizeHeight)
56+
let normalizedNose = CGPoint(
57+
x: 0.5283648181467697 * normalizeWidth,
58+
y: 0.5319401621818542 * normalizeHeight
59+
)
60+
let normalizedMouth = CGPoint(
61+
x: 0.5062596005080024 * normalizeWidth,
62+
y: 0.689265251159668 * normalizeHeight
63+
)
64+
let normalizedRightEar = CGPoint(
65+
x: 0.1658528943614037 * normalizeWidth,
66+
y: 0.5668278932571411 * normalizeHeight
67+
)
68+
let normalizedLeftEar = CGPoint(
69+
x: 0.7898947484263203 * normalizeWidth,
70+
y: 0.5973731875419617 * normalizeHeight
71+
)
72+
73+
expectedNormalizeFace = DetectedFace(
74+
boundingBox: normalizedBoundingBox,
75+
leftEye: normalizedLeftEye,
76+
rightEye: normalizedRightEye,
77+
nose: normalizedNose,
78+
mouth: normalizedMouth,
79+
rightEar: normalizedRightEar,
80+
leftEar: normalizedLeftEar,
81+
confidence: confidence
82+
)
83+
}
84+
85+
/// Given: A `DetectedFace`
86+
/// When: when the struct is initialized
87+
/// Then: the calculated landmarks are available and calculated as expected
88+
func testDetectedFaceLandmarks() {
89+
XCTAssertEqual(detectedFace.eyeCenterX, 0.5128901207314964)
90+
XCTAssertEqual(detectedFace.eyeCenterY, 0.4769134223461151)
91+
XCTAssertEqual(detectedFace.faceDistance, 0.31218859419592454)
92+
XCTAssertEqual(detectedFace.pupilDistance, 0.31218859419592454)
93+
XCTAssertEqual(detectedFace.faceHeight, 0.21245532000610062)
94+
}
95+
96+
/// Given: A `DetectedFace`
97+
/// When: when boundingBoxFromLandmarks is called
98+
/// Then: the calculated bounding box is returned
99+
func testDetectedFaceBoundingBoxFromLandmarks() {
100+
let ovalRect = CGRect.zero
101+
let expectedBoundingBox = CGRect(
102+
x: 0.1658528943614037,
103+
y: 0.041756969751750916,
104+
width: 0.6240418540649166,
105+
height: 0.8457092820983773
106+
)
107+
let boundingBox = detectedFace.boundingBoxFromLandmarks(ovalRect: ovalRect)
108+
XCTAssertEqual(boundingBox.origin.x, expectedBoundingBox.origin.x)
109+
XCTAssertEqual(boundingBox.origin.y, expectedBoundingBox.origin.y)
110+
XCTAssertEqual(boundingBox.width, expectedBoundingBox.width)
111+
XCTAssertEqual(boundingBox.height, expectedBoundingBox.height)
112+
}
113+
114+
/// Given: A `DetectedFace`
115+
/// When: when normalize is called with a view dimension
116+
/// Then: the normalized face calculates the correct landmark distances
117+
func testDetectedFaceNormalize() {
118+
let normalizedFace = detectedFace.normalize(width: normalizeWidth, height: normalizeHeight)
119+
XCTAssertEqual(normalizedFace.eyeCenterX, expectedNormalizeFace.eyeCenterX)
120+
XCTAssertEqual(normalizedFace.eyeCenterY, expectedNormalizeFace.eyeCenterY)
121+
XCTAssertEqual(normalizedFace.faceDistance, expectedNormalizeFace.faceDistance)
122+
XCTAssertEqual(normalizedFace.pupilDistance, expectedNormalizeFace.pupilDistance)
123+
XCTAssertEqual(normalizedFace.faceHeight, expectedNormalizeFace.faceHeight)
124+
}
125+
126+
}

Tests/FaceLivenessTests/LivenessTests.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,9 @@ final class FaceLivenessDetectionViewModelTestCase: XCTestCase {
126126
let rightEye = CGPoint(x: 0.38036393762719456, y: 0.48050540685653687)
127127
let nose = CGPoint(x: 0.48489856674964926, y: 0.54713362455368042)
128128
let mouth = CGPoint(x: 0.47411978167652435, y: 0.63170802593231201)
129-
let detectedFace = DetectedFace(boundingBox: boundingBox, leftEye: leftEye, rightEye: rightEye, nose: nose, mouth: mouth, confidence: 0.971859633)
129+
let leftEar = CGPoint(x: 0.7898947484263203, y: 0.5973731875419617)
130+
let rightEar = CGPoint(x: 0.1658528943614037, y: 0.5668278932571411)
131+
let detectedFace = DetectedFace(boundingBox: boundingBox, leftEye: leftEye, rightEye: rightEye, nose: nose, mouth: mouth, rightEar: rightEar, leftEar: leftEar, confidence: 0.971859633)
130132
viewModel.process(newResult: .singleFace(detectedFace))
131133
try await Task.sleep(seconds: 1)
132134

0 commit comments

Comments
 (0)