Skip to content

Commit

Permalink
chore: update face bounding box landmark calculations (#77)
Browse files Browse the repository at this point in the history
* chore: update face bounding box landmark calculations

* fix bounding box width and height

* fix typo on bounding box right
  • Loading branch information
phantumcode authored Nov 29, 2023
1 parent 471ac56 commit ba5e1ae
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 38 deletions.
98 changes: 62 additions & 36 deletions Sources/FaceLiveness/FaceDetection/BlazeFace/DetectedFace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,53 +14,79 @@ struct DetectedFace {
let rightEye: CGPoint
let nose: CGPoint
let mouth: CGPoint
let rightEar: CGPoint
let leftEar: CGPoint

let confidence: Float

func boundingBoxFromLandmarks() -> CGRect {
let eyeCenterX = (leftEye.x + rightEye.x) / 2
let eyeCenterY = (leftEye.y + rightEye.y) / 2

let cx = (nose.x + eyeCenterX) / 2
let cy = (nose.y + eyeCenterY) / 2

let ow = sqrt(pow((leftEye.x - rightEye.x), 2) + pow((leftEye.y - rightEye.y), 2)) * 2
let oh = 1.618 * ow
let minX = cx - ow / 2
let minY = cy - oh / 2

let rect = CGRect(x: minX, y: minY, width: ow, height: oh)
func boundingBoxFromLandmarks(ovalRect: CGRect) -> CGRect {
let alpha = 2.0
let gamma = 1.8
let ow = (alpha * pupilDistance + gamma * faceHeight) / 2
var cx = (eyeCenterX + nose.x) / 2

if ovalRect != CGRect.zero {
let ovalTop = ovalRect.minY
let ovalHeight = ovalRect.maxY - ovalRect.minY
if eyeCenterY > (ovalTop + ovalHeight) / 2 {
cx = eyeCenterX
}
}

let faceWidth = ow
let faceHeight = 1.68 * faceWidth
let faceBoxBottom = boundingBox.maxY
let faceBoxTop = faceBoxBottom - faceHeight
let faceBoxLeft = min(cx - ow / 2, rightEar.x)
let faceBoxRight = max(cx + ow / 2, leftEar.x)
let width = faceBoxRight - faceBoxLeft
let height = faceBoxBottom - faceBoxTop
let rect = CGRect(x: faceBoxLeft, y: faceBoxTop, width: width, height: height)
return rect
}

var faceDistance: CGFloat {
sqrt(pow(rightEye.x - leftEye.x, 2) + pow(rightEye.y - leftEye.y, 2))
}

var pupilDistance: CGFloat {
sqrt(pow(leftEye.x - rightEye.x, 2) + pow(leftEye.y - rightEye.y, 2))
}

var eyeCenterX: CGFloat {
(leftEye.x + rightEye.x) / 2
}

var eyeCenterY: CGFloat {
(leftEye.y + rightEye.y) / 2
}

var faceHeight: CGFloat {
sqrt(pow(eyeCenterX - mouth.x, 2) + pow(eyeCenterY - mouth.y, 2))
}

func normalize(width: CGFloat, height: CGFloat) -> DetectedFace {
.init(
boundingBox: .init(
x: boundingBox.minX * width,
y: boundingBox.minY * height,
width: boundingBox.width * width,
height: boundingBox.height * height
),
leftEye: .init(
x: leftEye.x * width,
y: leftEye.y * height
),
rightEye: .init(
x: rightEye.x * width,
y: rightEye.y * height
),
nose: .init(
x: nose.x * width,
y: nose.y * height
),
mouth: .init(
x: mouth.x * width,
y: mouth.y * height
),
let boundingBox = CGRect(
x: boundingBox.minX * width,
y: boundingBox.minY * height,
width: boundingBox.width * width,
height: boundingBox.height * height
)
let leftEye = CGPoint(x: leftEye.x * width, y: leftEye.y * height)
let rightEye = CGPoint(x: rightEye.x * width, y: rightEye.y * height)
let nose = CGPoint(x: nose.x * width, y: nose.y * height)
let mouth = CGPoint(x: mouth.x * width, y: mouth.y * height)
let rightEar = CGPoint(x: rightEar.x * width, y: rightEar.y * height)
let leftEar = CGPoint(x: leftEar.x * width, y: leftEar.y * height)

return DetectedFace(
boundingBox: boundingBox,
leftEye: leftEye,
rightEye: rightEye,
nose: nose,
mouth: mouth,
rightEar: rightEar,
leftEar: leftEar,
confidence: confidence
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ extension FaceDetectorShortRange {
let leftEye = faceResult[3]
let nose = faceResult[4]
let mouth = faceResult[5]
let rightEar = faceResult[6]
let leftEar = faceResult[7]



let boundingBox = CGRect(
Expand All @@ -172,6 +175,8 @@ extension FaceDetectorShortRange {
rightEye: .init(x: rightEye.x, y: rightEye.y),
nose: .init(x: nose.x, y: nose.y),
mouth: .init(x: mouth.x, y: mouth.y),
rightEar: .init(x: rightEar.x, y: rightEar.y),
leftEar: .init(x: leftEar.x, y: leftEar.y),
confidence: overlappingConfidenceScore / Float(overlappingOutputs.count)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ extension FaceLivenessDetectionViewModel: FaceDetectionResultHandler {
}
case .singleFace(let face):
var normalizedFace = normalizeFace(face)
normalizedFace.boundingBox = normalizedFace.boundingBoxFromLandmarks()
normalizedFace.boundingBox = normalizedFace.boundingBoxFromLandmarks(ovalRect: ovalRect)

switch livenessState.state {
case .pendingFacePreparedConfirmation:
Expand Down
126 changes: 126 additions & 0 deletions Tests/FaceLivenessTests/DetectedFaceTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import XCTest
@testable import FaceLiveness


final class DetectedFaceTests: XCTestCase {
var detectedFace: DetectedFace!
var expectedNormalizeFace: DetectedFace!
let normalizeWidth = 414.0
let normalizeHeight = 552.0

override func setUp() {
let boundingBox = CGRect(
x: 0.15805082494171963,
y: 0.3962942063808441,
width: 0.6549023386310235,
height: 0.49117204546928406
)
let leftEye = CGPoint(x: 0.6686329891870315, y: 0.48738187551498413)
let rightEye = CGPoint(x: 0.35714725227596134, y: 0.4664449691772461)
let nose = CGPoint(x: 0.5283648181467697, y: 0.5319401621818542)
let mouth = CGPoint(x: 0.5062596005080024, y: 0.689265251159668)
let rightEar = CGPoint(x: 0.1658528943614037, y: 0.5668278932571411)
let leftEar = CGPoint(x: 0.7898947484263203, y: 0.5973731875419617)
let confidence: Float = 0.94027895
detectedFace = DetectedFace(
boundingBox: boundingBox,
leftEye: leftEye,
rightEye: rightEye,
nose: nose,
mouth: mouth,
rightEar: rightEar,
leftEar: leftEar,
confidence: confidence
)

let normalizedBoundingBox = CGRect(
x: 0.15805082494171963 * normalizeWidth,
y: 0.3962942063808441 * normalizeHeight,
width: 0.6549023386310235 * normalizeWidth,
height: 0.49117204546928406 * normalizeHeight
)
let normalizedLeftEye = CGPoint(
x: 0.6686329891870315 * normalizeWidth,
y: 0.48738187551498413 * normalizeHeight
)
let normalizedRightEye = CGPoint(
x: 0.35714725227596134 * normalizeWidth,
y: 0.4664449691772461 * normalizeHeight)
let normalizedNose = CGPoint(
x: 0.5283648181467697 * normalizeWidth,
y: 0.5319401621818542 * normalizeHeight
)
let normalizedMouth = CGPoint(
x: 0.5062596005080024 * normalizeWidth,
y: 0.689265251159668 * normalizeHeight
)
let normalizedRightEar = CGPoint(
x: 0.1658528943614037 * normalizeWidth,
y: 0.5668278932571411 * normalizeHeight
)
let normalizedLeftEar = CGPoint(
x: 0.7898947484263203 * normalizeWidth,
y: 0.5973731875419617 * normalizeHeight
)

expectedNormalizeFace = DetectedFace(
boundingBox: normalizedBoundingBox,
leftEye: normalizedLeftEye,
rightEye: normalizedRightEye,
nose: normalizedNose,
mouth: normalizedMouth,
rightEar: normalizedRightEar,
leftEar: normalizedLeftEar,
confidence: confidence
)
}

/// Given: A `DetectedFace`
/// When: when the struct is initialized
/// Then: the calculated landmarks are available and calculated as expected
func testDetectedFaceLandmarks() {
XCTAssertEqual(detectedFace.eyeCenterX, 0.5128901207314964)
XCTAssertEqual(detectedFace.eyeCenterY, 0.4769134223461151)
XCTAssertEqual(detectedFace.faceDistance, 0.31218859419592454)
XCTAssertEqual(detectedFace.pupilDistance, 0.31218859419592454)
XCTAssertEqual(detectedFace.faceHeight, 0.21245532000610062)
}

/// Given: A `DetectedFace`
/// When: when boundingBoxFromLandmarks is called
/// Then: the calculated bounding box is returned
func testDetectedFaceBoundingBoxFromLandmarks() {
let ovalRect = CGRect.zero
let expectedBoundingBox = CGRect(
x: 0.1658528943614037,
y: 0.041756969751750916,
width: 0.6240418540649166,
height: 0.8457092820983773
)
let boundingBox = detectedFace.boundingBoxFromLandmarks(ovalRect: ovalRect)
XCTAssertEqual(boundingBox.origin.x, expectedBoundingBox.origin.x)
XCTAssertEqual(boundingBox.origin.y, expectedBoundingBox.origin.y)
XCTAssertEqual(boundingBox.width, expectedBoundingBox.width)
XCTAssertEqual(boundingBox.height, expectedBoundingBox.height)
}

/// Given: A `DetectedFace`
/// When: when normalize is called with a view dimension
/// Then: the normalized face calculates the correct landmark distances
func testDetectedFaceNormalize() {
let normalizedFace = detectedFace.normalize(width: normalizeWidth, height: normalizeHeight)
XCTAssertEqual(normalizedFace.eyeCenterX, expectedNormalizeFace.eyeCenterX)
XCTAssertEqual(normalizedFace.eyeCenterY, expectedNormalizeFace.eyeCenterY)
XCTAssertEqual(normalizedFace.faceDistance, expectedNormalizeFace.faceDistance)
XCTAssertEqual(normalizedFace.pupilDistance, expectedNormalizeFace.pupilDistance)
XCTAssertEqual(normalizedFace.faceHeight, expectedNormalizeFace.faceHeight)
}

}
4 changes: 3 additions & 1 deletion Tests/FaceLivenessTests/LivenessTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ final class FaceLivenessDetectionViewModelTestCase: XCTestCase {
let rightEye = CGPoint(x: 0.38036393762719456, y: 0.48050540685653687)
let nose = CGPoint(x: 0.48489856674964926, y: 0.54713362455368042)
let mouth = CGPoint(x: 0.47411978167652435, y: 0.63170802593231201)
let detectedFace = DetectedFace(boundingBox: boundingBox, leftEye: leftEye, rightEye: rightEye, nose: nose, mouth: mouth, confidence: 0.971859633)
let leftEar = CGPoint(x: 0.7898947484263203, y: 0.5973731875419617)
let rightEar = CGPoint(x: 0.1658528943614037, y: 0.5668278932571411)
let detectedFace = DetectedFace(boundingBox: boundingBox, leftEye: leftEye, rightEye: rightEye, nose: nose, mouth: mouth, rightEar: rightEar, leftEar: leftEar, confidence: 0.971859633)
viewModel.process(newResult: .singleFace(detectedFace))
try await Task.sleep(seconds: 1)

Expand Down

0 comments on commit ba5e1ae

Please sign in to comment.