Skip to content

Commit df30e86

Browse files
authored
chore: kickoff release
2 parents 615ccd8 + eb2e037 commit df30e86

40 files changed

+749
-464
lines changed

HostApp/HostApp/Views/LivenessResultContentView+Result.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,19 @@ extension LivenessResultContentView {
1414
let valueTextColor: Color
1515
let valueBackgroundColor: Color
1616
let auditImage: Data?
17-
17+
let isLive: Bool
18+
1819
init(livenessResult: LivenessResult) {
1920
guard livenessResult.confidenceScore > 0 else {
2021
text = ""
2122
value = ""
2223
valueTextColor = .clear
2324
valueBackgroundColor = .clear
2425
auditImage = nil
26+
isLive = false
2527
return
2628
}
27-
29+
isLive = livenessResult.isLive
2830
let truncated = String(format: "%.4f", livenessResult.confidenceScore)
2931
value = truncated
3032
if livenessResult.isLive {

HostApp/HostApp/Views/LivenessResultContentView.swift

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@ struct LivenessResultContentView: View {
1717
Text("Result:")
1818
Text(result.text)
1919
.fontWeight(.semibold)
20-
2120
}
22-
.padding(.bottom, 12)
2321

2422
HStack {
2523
Text("Liveness confidence score:")
@@ -42,6 +40,20 @@ struct LivenessResultContentView: View {
4240
.frame(maxWidth: .infinity, idealHeight: 268)
4341
.background(Color.secondary.opacity(0.1))
4442
}
43+
44+
if !result.isLive {
45+
steps()
46+
.padding()
47+
.background(
48+
Rectangle()
49+
.foregroundColor(
50+
.dynamicColors(
51+
light: .hex("#ECECEC"),
52+
dark: .darkGray
53+
)
54+
)
55+
.cornerRadius(6))
56+
}
4557
}
4658
.padding(.bottom, 16)
4759
.onAppear {
@@ -54,6 +66,29 @@ struct LivenessResultContentView: View {
5466
}
5567
}
5668
}
69+
70+
private func steps() -> some View {
71+
func step(number: Int, text: String) -> some View {
72+
HStack(alignment: .top) {
73+
Text("\(number).")
74+
Text(text)
75+
}
76+
}
77+
78+
return VStack(
79+
alignment: .leading,
80+
spacing: 8
81+
) {
82+
Text("Tips to pass the video check:")
83+
.fontWeight(.semibold)
84+
85+
step(number: 1, text: "Avoid very bright lighting conditions, such as direct sunlight.")
86+
.accessibilityElement(children: .combine)
87+
88+
step(number: 2, text: "Remove sunglasses, mask, hat, or anything blocking your face.")
89+
.accessibilityElement(children: .combine)
90+
}
91+
}
5792
}
5893

5994

HostApp/HostApp/Views/LivenessResultView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ struct LivenessResultView<Content: View>: View {
1515
@State var displayingCopiedNotification = false
1616

1717
init(
18-
title: String = "Liveness Check",
18+
title: String = "Liveness Result",
1919
sessionID: String,
2020
onTryAgain: @escaping () -> Void,
2121
@ViewBuilder content: () -> Content

HostApp/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ cd amplify-ui-swift-livenes/HostApp
2929

3030
7. Once signed in and authenticated, the "Create Liveness Session" is enabled. Click the button to generate and get a session id from your backend.
3131

32-
8. Once a session id is created, the Liveness Check screen is displayed. Follow the instructions and click on Begin Check button to begin liveness verification.
32+
8. Once a session id is created, the Liveness Check screen is displayed. Follow the instructions and click on Start video check button to begin liveness verification.
3333

3434
## Provision AWS Backend Resources
3535

Sources/FaceLiveness/AV/CMSampleBuffer+Rotate.swift

Lines changed: 0 additions & 42 deletions
This file was deleted.

Sources/FaceLiveness/AV/LivenessCaptureSession.swift

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,34 @@ import AVFoundation
1111
class LivenessCaptureSession {
1212
let captureDevice: LivenessCaptureDevice
1313
private let captureQueue = DispatchQueue(label: "com.amazonaws.faceliveness.cameracapturequeue")
14-
let outputDelegate: OutputSampleBufferCapturer
14+
let outputDelegate: AVCaptureVideoDataOutputSampleBufferDelegate
1515
var captureSession: AVCaptureSession?
16+
17+
var outputSampleBufferCapturer: OutputSampleBufferCapturer? {
18+
return outputDelegate as? OutputSampleBufferCapturer
19+
}
1620

17-
init(captureDevice: LivenessCaptureDevice, outputDelegate: OutputSampleBufferCapturer) {
21+
init(captureDevice: LivenessCaptureDevice, outputDelegate: AVCaptureVideoDataOutputSampleBufferDelegate) {
1822
self.captureDevice = captureDevice
1923
self.outputDelegate = outputDelegate
2024
}
2125

2226
func startSession(frame: CGRect) throws -> CALayer {
27+
try startSession()
28+
29+
guard let captureSession = captureSession else {
30+
throw LivenessCaptureSessionError.captureSessionUnavailable
31+
}
32+
33+
let previewLayer = previewLayer(
34+
frame: frame,
35+
for: captureSession
36+
)
37+
38+
return previewLayer
39+
}
40+
41+
func startSession() throws {
2342
guard let camera = captureDevice.avCaptureDevice
2443
else { throw LivenessCaptureSessionError.cameraUnavailable }
2544

@@ -44,17 +63,10 @@ class LivenessCaptureSession {
4463
captureSession.startRunning()
4564
}
4665

47-
let previewLayer = previewLayer(
48-
frame: frame,
49-
for: captureSession
50-
)
51-
5266
videoOutput.setSampleBufferDelegate(
5367
outputDelegate,
5468
queue: captureQueue
5569
)
56-
57-
return previewLayer
5870
}
5971

6072
func stopRunning() {
@@ -83,6 +95,11 @@ class LivenessCaptureSession {
8395
_ output: AVCaptureVideoDataOutput,
8496
for captureSession: AVCaptureSession
8597
) throws {
98+
if captureSession.canAddOutput(output) {
99+
captureSession.addOutput(output)
100+
} else {
101+
throw LivenessCaptureSessionError.captureSessionOutputUnavailable
102+
}
86103
output.videoSettings = [
87104
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA
88105
]
@@ -92,12 +109,6 @@ class LivenessCaptureSession {
92109
.forEach {
93110
$0.videoOrientation = .portrait
94111
}
95-
96-
if captureSession.canAddOutput(output) {
97-
captureSession.addOutput(output)
98-
} else {
99-
throw LivenessCaptureSessionError.captureSessionOutputUnavailable
100-
}
101112
}
102113

103114
private func previewLayer(

Sources/FaceLiveness/AV/OutputSampleBufferCapturer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class OutputSampleBufferCapturer: NSObject, AVCaptureVideoDataOutputSampleBuffer
2424
) {
2525
videoChunker.consume(sampleBuffer)
2626

27-
guard let imageBuffer = sampleBuffer.rotateRightUpMirrored()
27+
guard let imageBuffer = sampleBuffer.imageBuffer
2828
else { return }
2929

3030
faceDetector.detectFaces(from: imageBuffer)

Sources/FaceLiveness/AV/VideoChunker.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ final class VideoChunker {
3434

3535
func start() {
3636
guard state == .pending else { return }
37-
state = .writing
3837
assetWriter.startWriting()
3938
assetWriter.startSession(atSourceTime: .zero)
39+
state = .writing
4040
}
4141

4242
func finish(singleFrame: @escaping (UIImage) -> Void) {
@@ -49,8 +49,8 @@ final class VideoChunker {
4949

5050
func consume(_ buffer: CMSampleBuffer) {
5151
if state == .awaitingSingleFrame {
52-
guard let rotated = buffer.rotateRightUpMirrored() else { return }
53-
let singleFrame = singleFrame(from: rotated)
52+
guard let imageBuffer = buffer.imageBuffer else { return }
53+
let singleFrame = singleFrame(from: imageBuffer)
5454
provideSingleFrame?(singleFrame)
5555
state = .complete
5656
}
@@ -66,10 +66,10 @@ final class VideoChunker {
6666
if assetWriterInput.isReadyForMoreMediaData {
6767
let timestamp = CMSampleBufferGetPresentationTimeStamp(buffer).seconds
6868
let presentationTime = CMTime(seconds: timestamp - startTimeSeconds, preferredTimescale: 600)
69-
guard let rotated = buffer.rotateRightUpMirrored() else { return }
69+
guard let imageBuffer = buffer.imageBuffer else { return }
7070

7171
pixelBufferAdaptor.append(
72-
rotated,
72+
imageBuffer,
7373
withPresentationTime: presentationTime
7474
)
7575
}

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.618 * 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
}

0 commit comments

Comments
 (0)