Vision framework can recognize objects in pictures such as faces. Vision API is only available since iOS 11. Some of useful possibilities of that framework:
First step is drag and drop a picture into the Assets folder. Make sure that picture contains a face.
Add some views to the ContentView
import SwiftUI import Vision struct ContentView: View { @State var img = UIImage(named: "face")! var body: some View { NavigationView { VStack { Image(uiImage: img) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 200, height: 200) Button { recognizeImage(image: img) } label: { Text("Recognize") }.padding() } }.navigationTitle("Facial Recognition") } }
The Image
view displays an image stored in the Assets folder. The .resizable()
modifier makes sure the image can be resized, the .aspectRatio(contentMode: .fit)
makes the image fit within the frame of the Image
view, and the .frame(width: 200, height: 200)
modifier defines the size of the Image
view.
When the user selects this Button
, it will call a function called recognizeImage
.
Add following functions
func recognizeImage(image: UIImage) { let handler = VNImageRequestHandler(cgImage: image.cgImage!, options: [:]) let request = VNDetectFaceRectanglesRequest(completionHandler: handleRecognize) try! handler.perform([request]) } func handleRecognize(request: VNRequest, error: Error?) { guard let results = request.results as? [VNFaceObservation] else { fatalError ("Can't find a face in the picture") } print("Found \(results.count) face(s)") }
This function uses VNDetectFaceRectanglesRequest
in the Vision framework to detect faces in a image. After it analyzes a picture, it runs another function called handleRecognize
.
This handleRecognize
function simply displays the number of faces. Keep in mind that the facial recognition feature may not always be accurate.
Highlight face
You can also highlight each face with a rectangle to show you exactly which parts of a picture the Vision framework recognized as a face. Vision framework provides a landmark data which containts information about face position.
func handleRecognizeAndHighlight(request: VNRequest, error: Error?) { guard let results = request.results as? [VNFaceObservation] else { fatalError ("Can't find a face in the picture") } print("Found \(results.count) face(s)") DispatchQueue.global().async { for landmark in results { highlightFace(with: landmark) } } } func highlightFace(with landmark: VNFaceObservation) { let source = img let boundary = landmark.boundingBox UIGraphicsBeginImageContextWithOptions(source.size, false, 1) let context = UIGraphicsGetCurrentContext()! context.setShouldAntialias(true) context.setAllowsAntialiasing(true) context.translateBy(x: 0, y: source.size.height) context.scaleBy(x: 1.0, y: -1.0) context.setLineJoin(.round) context.setLineCap(.round) let rect = CGRect(x: 0, y:0, width: source.size.width, height: source.size.height) context.draw(source.cgImage!, in: rect) let fillColor: UIColor = .green fillColor.setStroke() let rectangleWidth = source.size.width * boundary.size.width let rectangleHeight = source.size.height * boundary.size.height context.setLineWidth(5) context.addRect(CGRect(x: boundary.origin.x * source.size.width, y:boundary.origin.y * source.size.height, width: rectangleWidth, height: rectangleHeight)) context.drawPath(using: CGPathDrawingMode.stroke) let highlightedImage : UIImage = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() DispatchQueue.main.async { img = highlightedImage } }
Centering image on a face
So far we can identify a face on a picture and process it position. Let's extend our knowledge and add posibility to crop the accordingly to lendmark (face position data). Initial solution.
Extension to UIImage
enum FaceCropError: Error { case notFound case unknown(Error) } extension UIImage { func cropToFace(completion: @escaping (Result<UIImage, FaceCropError>) -> Void) { let req = VNDetectFaceRectanglesRequest { request, error in if let error = error { completion(.failure(.unknown(error))) return } guard let results = request.results as? [VNFaceObservation] else { completion(.failure(.notFound)) return } let rect = self.getRectangle(for: results) guard let cgImg = self.cgImage, let result = cgImg.cropping(to: rect) else { completion(.failure(.notFound)) return } completion(.success(UIImage(cgImage: result))) } do { try VNImageRequestHandler(cgImage: self.cgImage!, options: [:]).perform([req]) } catch let error { completion(.failure(.unknown(error))) } } private func getRectangle(for faces: [VNFaceObservation]) -> CGRect { let marginX: CGFloat = 50 let marginY: CGFloat = 100 let imageWidth = CGFloat(self.cgImage!.width) let imageHeight = CGFloat(self.cgImage!.height) var totalX: CGFloat = 0 var totalY: CGFloat = 0 var totalW: CGFloat = 0 var totalH: CGFloat = 0 var minX = CGFloat.greatestFiniteMagnitude var minY = CGFloat.greatestFiniteMagnitude let numFaces = CGFloat(faces.count) for face in faces { let w = face.boundingBox.width * imageWidth let h = face.boundingBox.height * imageHeight let x = face.boundingBox.origin.x * imageWidth let y = (1 - face.boundingBox.origin.y) * imageHeight - h totalX += x totalY += y totalW += w totalH += h minX = .minimum(minX, x) minY = .minimum(minY, y) } let avgX = totalX / numFaces let avgY = totalY / numFaces let avgW = totalW / numFaces let avgH = totalH / numFaces let offsetX = marginX + avgX - minX let offsetY = marginY + avgY - minY return CGRect(x: avgX - offsetX, y: avgY - offsetY, width: avgW + (offsetX * 2), height: avgH + (offsetY * 2)) } }
Usage
struct ContentView: View { @State var img = UIImage(named: "face")! var body: some View { NavigationView { VStack { Image(uiImage: img) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 200, height: 200) Button { cropToFace() } label: { Text("Recognize") }.padding() } }.navigationTitle("Facial Recognition") } func cropToFace() { DispatchQueue.global().async { img.cropToFace { result in switch result { case .success(let image): DispatchQueue.main.async { img = image } case .failure(let error): DispatchQueue.main.async { print(error) } } } } } }