I am using Google’s MLKit and MLKitFaceDetection for face detection. This code does detect the face in real time with the camera session, but I would like it to detect a centered face and provide instructions to the user on how to arrange their face to center it. I share the view controller code where this objective must be carried out. In addition to this, I would like a red square to be drawn for when a face is detected:
import UIKit
import AVFoundation
import MLKit
import MLKitFaceDetection
class FaceProgressViewController: UIViewController{
@IBOutlet weak var progressBarView: CircularProgressBarView!
@IBOutlet weak var previewView: PreviewView!
private let captureSession = AVCaptureSession()
private let captureOutput = AVCapturePhotoOutput()
private let sessionQueue = DispatchQueue(label: "capture_queue")
private var options = FaceDetectorOptions()
private let distanceToCamera: CGFloat = 0.0
private var faceDetector: FaceDetector?
override func viewDidLoad() {
super.viewDidLoad()
options.performanceMode = .accurate
options.landmarkMode = .all
options.classificationMode = .all
previewView.layer.cornerRadius = previewView.frame.size.width/2
previewView.clipsToBounds = true
setup()
}
private func setup() {
previewView.session = captureSession
sessionQueue.async {
self.setupSession()
self.captureSession.startRunning()
self.setupFaceDetector()
}
}
private func setupSession() {
guard let captureDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) else {
return
}
captureSession.beginConfiguration()
do {
let input = try AVCaptureDeviceInput(device: captureDevice)
if let session = previewView.session, session.canAddInput(input) {
session.addInput(input)
let output = AVCaptureVideoDataOutput()
output.setSampleBufferDelegate(self, queue: DispatchQueue(label: "video_queue"))
if session.canAddOutput(output) {
session.addOutput(output)
}
}
} catch {
print("Error al configurar la sesión de captura: \(error.localizedDescription)")
}
if !captureSession.outputs.isEmpty {
captureSession.outputs.forEach { output in
captureSession.removeOutput(output)
}
}
let videoOutput = AVCaptureVideoDataOutput()
videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "video_queue"))
if captureSession.canAddOutput(videoOutput) {
captureSession.addOutput(videoOutput)
}
captureSession.commitConfiguration()
}
private func setupFaceDetector() {
self.faceDetector = FaceDetector.faceDetector(options: options)
}
private func calculateDistance(_ faceRect: CGRect, pixelBuffer: CVPixelBuffer) -> CGFloat {
let cameraFieldOfView: CGFloat = 60.0
let faceRealSize = tan(degreesToRadians(cameraFieldOfView) / 2.0) * 2.0 * distanceToCamera
let distance = faceRealSize / faceRect.size.width
return distance
}
private func degreesToRadians(_ degrees: CGFloat) -> CGFloat {
return degrees * .pi / 180.0
}
}
extension FaceProgressViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
return
}
let visionImage = VisionImage(buffer: sampleBuffer)
visionImage.orientation = .up
self.faceDetector?.process(visionImage) { (faces, error) in
if let error = error {
print("Error en la detección facial: \(error.localizedDescription)")
return
}
if let detectedFaces = faces, !detectedFaces.isEmpty {
if let face = detectedFaces.first {
let faceRect = face.frame
DispatchQueue.main.async {
self.drawFaceRect(faceRect)
}
print("Se detectó un rostro en el cuadro: \(faceRect)")
}
} else {
DispatchQueue.main.async {
self.clearFaceRect()
}
print("No se detectaron rostros en el cuadro.")
}
}
}
private func drawFaceRect(_ rect: CGRect) {
if let sublayers = previewView.layer.sublayers {
for layer in sublayers {
if layer.name == "faceRectLayer" {
layer.removeFromSuperlayer()
}
}
}
let faceRectLayer = CAShapeLayer()
faceRectLayer.name = "faceRectLayer"
faceRectLayer.strokeColor = UIColor.red.cgColor
faceRectLayer.fillColor = UIColor.clear.cgColor
faceRectLayer.lineWidth = 2.0
let path = UIBezierPath(rect: rect)
faceRectLayer.path = path.cgPath
previewView.layer.addSublayer(faceRectLayer)
}
private func clearFaceRect() {
previewView.clearFaceRect()
}
}
This is the code for the PreviewView class:
import UIKit
import AVFoundation
class PreviewView: UIView {
// MARK: - Properties
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
guard let layer = layer as? AVCaptureVideoPreviewLayer else {
fatalError("Expected `AVCaptureVideoPreviewLayer`")
}
return layer
}
var session: AVCaptureSession? {
get { videoPreviewLayer.session }
set { videoPreviewLayer.session = newValue }
}
private var faceRectLayer: CAShapeLayer?
// MARK: - Override
override class var layerClass: AnyClass {
AVCaptureVideoPreviewLayer.self
}
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
// Configura la escala de la vista previa para que llene la vista
videoPreviewLayer.videoGravity = .resizeAspectFill
faceRectLayer = CAShapeLayer()
faceRectLayer?.strokeColor = UIColor.red.cgColor // Puedes ajustar el color
faceRectLayer?.fillColor = UIColor.clear.cgColor
faceRectLayer?.lineWidth = 2.0 // Puedes ajustar el grosor de línea
layer.addSublayer(faceRectLayer!)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// Configura la escala de la vista previa para que llene la vista
videoPreviewLayer.videoGravity = .resizeAspectFill
// Configura la capa para el rectángulo del rostro
faceRectLayer = CAShapeLayer()
faceRectLayer?.strokeColor = UIColor.red.cgColor // Puedes ajustar el color
faceRectLayer?.fillColor = UIColor.clear.cgColor
faceRectLayer?.lineWidth = 2.0 // Puedes ajustar el grosor de línea
layer.addSublayer(faceRectLayer!)
}
func drawFaceRect(_ rect: CGRect, distance: CGFloat) {
CATransaction.begin()
CATransaction.setDisableActions(true)
// Calcula el tamaño del rectángulo del rostro en función de la distancia
let scaleFactor = max(1.0, distance)
let scaledRect = CGRect(
x: rect.origin.x * scaleFactor,
y: rect.origin.y * scaleFactor,
width: rect.size.width * scaleFactor,
height: rect.size.height * scaleFactor
)
let path = UIBezierPath(rect: scaledRect)
faceRectLayer?.path = path.cgPath
faceRectLayer?.isHidden = false
CATransaction.commit()
}
func clearFaceRect() {
CATransaction.begin()
CATransaction.setDisableActions(true)
faceRectLayer?.path = nil
faceRectLayer?.isHidden = true
CATransaction.commit()
}
}
The problem is that the square is not drawn in the preview, I would have to get too close to the camera for the square to be drawn but you can barely see it. The preview is set to a size of 260x260px. I can’t figure out how I can make it detect a centered face and provide instructions to the user to center their face in front of the camera.
I hope you can help me, thank you!