Detect the user's face and provide guidance to the user to center their face with MLKit and MLKitFaceDetection with Swift

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!

This topic was automatically closed after 166 days. New replies are no longer allowed.