Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add no-light/facemovementonly challenge support #131

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/aws-amplify/amplify-swift",
"state" : {
"revision" : "79d062d354bb190666774e8ef3c83ad52f023889",
"version" : "2.38.0"
"branch" : "feat/no-light-support",
"revision" : "22e02fa21399122aac1d8b4f6ab23c242c79dae6"
}
},
{
Expand Down Expand Up @@ -50,8 +50,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/stephencelis/SQLite.swift.git",
"state" : {
"revision" : "a95fc6df17d108bd99210db5e8a9bac90fe984b8",
"version" : "0.15.3"
"revision" : "5f5ad81ac0d0a0f3e56e39e646e8423c617df523",
"version" : "0.13.2"
}
},
{
Expand Down
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
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/aws-amplify/amplify-swift",
"state" : {
"revision" : "79d062d354bb190666774e8ef3c83ad52f023889",
"version" : "2.38.0"
"branch" : "feat/no-light-support",
"revision" : "614be628cb01188e519bb0e9e4d90bd83703d139"
}
},
{
Expand Down
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.38.0")
// 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))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import SwiftUI
import Combine
@_spi(PredictionsFaceLiveness) import AWSPredictionsPlugin

struct InstructionContainerView: View {
@ObservedObject var viewModel: FaceLivenessDetectionViewModel
Expand Down Expand Up @@ -97,13 +98,29 @@ struct InstructionContainerView: View {
argument: LocalizedStrings.challenge_verifying
)
}
case .faceMatched:
case .completedNoLightCheck:
InstructionView(
text: LocalizedStrings.challenge_instruction_hold_still,
backgroundColor: .livenessPrimaryBackground,
textColor: .livenessPrimaryLabel,
font: .title
text: LocalizedStrings.challenge_verifying,
backgroundColor: .livenessBackground
)
.onAppear {
UIAccessibility.post(
notification: .announcement,
argument: LocalizedStrings.challenge_verifying
)
}
case .faceMatched:
if let challenge = viewModel.challenge,
case .faceMovementAndLightChallenge = challenge.type {
InstructionView(
text: LocalizedStrings.challenge_instruction_hold_still,
backgroundColor: .livenessPrimaryBackground,
textColor: .livenessPrimaryLabel,
font: .title
)
} else {
EmptyView()
}
default:
EmptyView()
}
Expand Down
Loading
Loading