Skip to content

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions HostApp/HostApp/Model/LivenessResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
//

import Foundation
@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin

struct LivenessResult: Codable {
let auditImageBytes: String?
let confidenceScore: Double
let isLive: Bool
let challenge: Challenge?
}

extension LivenessResult: CustomDebugStringConvertible {
Expand All @@ -20,6 +22,8 @@ extension LivenessResult: CustomDebugStringConvertible {
- confidenceScore: \(confidenceScore)
- isLive: \(isLive)
- auditImageBytes: \(auditImageBytes == nil ? "nil" : "<placeholder>")
- challengeType: \(String(describing: challenge?.type))
- challengeVersion: \(String(describing: challenge?.version))
"""
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import SwiftUI
@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin

extension LivenessResultContentView {
struct Result {
Expand All @@ -15,6 +16,7 @@ extension LivenessResultContentView {
let valueBackgroundColor: Color
let auditImage: Data?
let isLive: Bool
let challenge: Challenge?

init(livenessResult: LivenessResult) {
guard livenessResult.confidenceScore > 0 else {
Expand All @@ -24,6 +26,7 @@ extension LivenessResultContentView {
valueBackgroundColor = .clear
auditImage = nil
isLive = false
challenge = nil
return
}
isLive = livenessResult.isLive
Expand All @@ -41,6 +44,7 @@ extension LivenessResultContentView {
auditImage = livenessResult.auditImageBytes.flatMap{
Data(base64Encoded: $0)
}
challenge = livenessResult.challenge
}
}

Expand Down
64 changes: 44 additions & 20 deletions HostApp/HostApp/Views/LivenessResultContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
//

import SwiftUI
@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin

struct LivenessResultContentView: View {
@State var result: Result = .init(livenessResult: .init(auditImageBytes: nil, confidenceScore: -1, isLive: false))
@State var result: Result = .init(livenessResult: .init(auditImageBytes: nil, confidenceScore: -1, isLive: false, challenge: nil))
let fetchResults: () async throws -> Result

var body: some View {
Expand Down Expand Up @@ -67,26 +68,48 @@ struct LivenessResultContentView: View {
}
}

func step(number: Int, text: String) -> some View {
HStack(alignment: .top) {
Text("\(number).")
Text(text)
}
}

@ViewBuilder
private func steps() -> some View {
func step(number: Int, text: String) -> some View {
HStack(alignment: .top) {
Text("\(number).")
Text(text)
switch result.challenge?.type {
case .faceMovementChallenge:
VStack(
alignment: .leading,
spacing: 8
) {
Text("Tips to pass the video check:")
.fontWeight(.semibold)

Text("Remove sunglasses, mask, hat, or anything blocking your face.")
.accessibilityElement(children: .combine)
}
case .faceMovementAndLightChallenge:
VStack(
alignment: .leading,
spacing: 8
) {
Text("Tips to pass the video check:")
.fontWeight(.semibold)

step(number: 1, text: "Avoid very bright lighting conditions, such as direct sunlight.")
.accessibilityElement(children: .combine)

step(number: 2, text: "Remove sunglasses, mask, hat, or anything blocking your face.")
.accessibilityElement(children: .combine)
}
case .none:
VStack(
alignment: .leading,
spacing: 8
) {
EmptyView()
}
}

return VStack(
alignment: .leading,
spacing: 8
) {
Text("Tips to pass the video check:")
.fontWeight(.semibold)

step(number: 1, text: "Avoid very bright lighting conditions, such as direct sunlight.")
.accessibilityElement(children: .combine)

step(number: 2, text: "Remove sunglasses, mask, hat, or anything blocking your face.")
.accessibilityElement(children: .combine)
}
}
}
Expand All @@ -99,7 +122,8 @@ extension LivenessResultContentView {
livenessResult: .init(
auditImageBytes: nil,
confidenceScore: 99.8329,
isLive: true
isLive: true,
challenge: nil
)
)
}
Expand Down
16 changes: 8 additions & 8 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ let package = Package(
targets: ["FaceLiveness"]),
],
dependencies: [
.package(url: "https://github.com/aws-amplify/amplify-swift", exact: "2.46.1")
// TODO: Change this before merge to main
.package(url: "https://github.com/aws-amplify/amplify-swift", branch: "feat/no-light-support")
],
targets: [
.target(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import Foundation
@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin

struct DetectedFace {
var boundingBox: CGRect
Expand All @@ -19,7 +20,8 @@ struct DetectedFace {

let confidence: Float

func boundingBoxFromLandmarks(ovalRect: CGRect) -> CGRect {
func boundingBoxFromLandmarks(ovalRect: CGRect,
ovalMatchChallenge: FaceLivenessSession.OvalMatchChallenge) -> CGRect {
let alpha = 2.0
let gamma = 1.8
let ow = (alpha * pupilDistance + gamma * faceHeight) / 2
Expand All @@ -34,7 +36,7 @@ struct DetectedFace {
}

let faceWidth = ow
let faceHeight = 1.618 * faceWidth
let faceHeight = ovalMatchChallenge.oval.heightWidthRatio * faceWidth
let faceBoxBottom = boundingBox.maxY
let faceBoxTop = faceBoxBottom - faceHeight
let faceBoxLeft = min(cx - ow / 2, rightEar.x)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Accelerate
import CoreGraphics
import CoreImage
import VideoToolbox
@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin

enum FaceDetectorShortRange {}

Expand All @@ -33,11 +34,16 @@ extension FaceDetectorShortRange {
)
}

weak var faceDetectionSessionConfiguration: FaceDetectionSessionConfigurationWrapper?
weak var detectionResultHandler: FaceDetectionResultHandler?

func setResultHandler(detectionResultHandler: FaceDetectionResultHandler) {
self.detectionResultHandler = detectionResultHandler
}

func setFaceDetectionSessionConfigurationWrapper(configuration: FaceDetectionSessionConfigurationWrapper) {
self.faceDetectionSessionConfiguration = configuration
}

func detectFaces(from buffer: CVPixelBuffer) {
let faces = prediction(for: buffer)
Expand Down Expand Up @@ -105,10 +111,17 @@ extension FaceDetectorShortRange {
count: confidenceScoresCapacity
)
)

let blazeFaceDetectionThreshold: Float
if let sessionConfiguration = faceDetectionSessionConfiguration?.sessionConfiguration {
blazeFaceDetectionThreshold = Float(sessionConfiguration.ovalMatchChallenge.faceDetectionThreshold)
} else {
blazeFaceDetectionThreshold = confidenceScoreThreshold
}

var passingConfidenceScoresIndices = confidenceScores
.enumerated()
.filter { $0.element >= confidenceScoreThreshold }
.filter { $0.element >= blazeFaceDetectionThreshold}
.sorted(by: {
$0.element > $1.element
})
Expand Down
5 changes: 5 additions & 0 deletions Sources/FaceLiveness/FaceDetection/FaceDetector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import AVFoundation
@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin

protocol FaceDetector {
func detectFaces(from buffer: CVPixelBuffer)
Expand All @@ -16,6 +17,10 @@ protocol FaceDetectionResultHandler: AnyObject {
func process(newResult: FaceDetectionResult)
}

protocol FaceDetectionSessionConfigurationWrapper: AnyObject {
var sessionConfiguration: FaceLivenessSession.SessionConfiguration? { get }
}

enum FaceDetectionResult {
case noFace
case singleFace(DetectedFace)
Expand Down
13 changes: 10 additions & 3 deletions Sources/FaceLiveness/Views/GetReadyPage/GetReadyPageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,21 @@
//

import SwiftUI
@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin

struct GetReadyPageView: View {
let beginCheckButtonDisabled: Bool
let onBegin: () -> Void

let challenge: Challenge

init(
onBegin: @escaping () -> Void,
beginCheckButtonDisabled: Bool = false
beginCheckButtonDisabled: Bool = false,
challenge: Challenge
) {
self.onBegin = onBegin
self.beginCheckButtonDisabled = beginCheckButtonDisabled
self.challenge = challenge
}

var body: some View {
Expand All @@ -30,6 +34,7 @@ struct GetReadyPageView: View {
popoverContent: { photosensitivityWarningPopoverContent }
)
.accessibilityElement(children: .combine)
.opacity(challenge.type == .faceMovementAndLightChallenge ? 1.0 : 0.0)
Text(LocalizedStrings.preview_center_your_face_text)
.font(.title)
.multilineTextAlignment(.center)
Expand Down Expand Up @@ -72,6 +77,8 @@ struct GetReadyPageView: View {

struct GetReadyPageView_Previews: PreviewProvider {
static var previews: some View {
GetReadyPageView(onBegin: {})
GetReadyPageView(onBegin: {},
challenge: .init(version: "2.0.0",
type: .faceMovementAndLightChallenge))
}
}
Loading