Swift Tutorial: Ứng dụng nhận diện khuôn mặt đơn giản (Phần 2)

Ở phần 1, ta đã nắm được:

  • Setup project và tích hợp thư viện ngoài bằng Cocoapods.
  • Thao tác cơ bản với Interface builder và Auto-layout tool. Phần 2 sẽ là về code camera và face recognition.

Bắt đầu phần 2, mình sẽ hướng dẫn cách tích hợp camera và chụp ảnh trong app.

Trước tiên chúng ta cần đọc tài liệu về camera của iOS device tại đây:
https://developer.apple.com/library/ios/documentation/AudioVideo/Conceptual/AVFoundationPG/Articles/04_MediaCapture.html

Theo tài liệu, chúng ta sử dụng thư viện AVFoundation để quản lý các chức năng về video và audio. Mình trích lại hình mô tả từ tài liệu:

AVFoundation

Để sử dụng chức năng chụp ảnh, chúng ta phải sử dụng 3 loại objects:

  • AVCaptureDevice Input (Video or audio)
  • AVCapture Output bao gồm: MovieFileOutput (quay phim), StillImageOutput (chụp ảnh tĩnh), VideoPreviewLayer (hiển thị preview video trên màn hình)
  • AVCapture Session (điểu khiển luồng dữ liệu giữa input và output)

Để có thể liên tục xác định vị trí miệng trên khuôn mặt, chúng ta cần phải liên tục xử lỹ dữ liệu hình ảnh từ camera của thiết bị. Phần cần chú ý chính là phần Processing Frames of Video trong tài liệu.

Tài liệu liên hệ đã có, ta có thể đọc qua và bắt đầu code luôn.

Bắt đầu từ project đã tạo từ bài trước. Bạn nào bắt đầu từ bài này thì down project về như sau

git clone https://github.com/muzix/goatcamera.git
cd goatcamera
pod install

Bước 1: Authorize Camera

Bắt đầu code, chúng ta sẽ tạo một class helper để gộp tất cả các công việc quản lý camera session vào một chỗ cho tiện quản lý và sử dụng lại ở project khác.

  • Chuột phải vào group GoatCamera -> Chọn Add Files to "GoatCamera"...
  • Chọn New folder -> Nhập tên: "Helpers" -> Add
  • Chuột phải thư mục Helpers vừa tạo -> Chọn New File... -> CocoaTouch Class -> Nhập tên Class: "CameraSession" -> Subclass of: NSObject -> Language: Swift -> Next -> Create NewFile

  • Trong file CameraSession, ta import thư viện cần dùng ở trên đầu như sau:

    import UIKit
    import AVFoundation
    import CoreMedia
    import CoreImage
    
  • Bên trong class CameraSession, ta khai báo các biến và delegate như sau:

    class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
    var session: AVCaptureSession!
    var sessionQueue: dispatch_queue_t!
    var videoDeviceInput: AVCaptureDeviceInput!
    var videoDeviceOutput: AVCaptureVideoDataOutput!
    var stillImageOutput: AVCaptureStillImageOutput!
    var runtimeErrorHandlingObserver: AnyObject?
    var cameraGranted: Bool!
    var isFrontCamera:Bool!
    }
    
  • Giải thích biến và ý nghĩa của ký tự ! và ? sau tên biến:
    Tài liệu Swift basic, mục Optionals
    Nói ngắn gọn, dấu ! định nghĩa biến đó là kiểu "implicitly unwrapped optionals". Tức là những biến này chắc chắn có giá trị khác nil, thường được gán giá trị ngay trong hàm khởi tạo của class.

  • Tiếp theo, viết hàm khởi tạo của class ngay dưới phần khai báo biến. Khởi tạo AVCaptureSession. SessionPreset ở độ phân giải 640x480. Khai báo độ phân giải ở mức trung bình sẽ cải thiện tốc độ nhận diện khuôn mặt mà vẫn có độ chính xác vừa phải. Hàm Deinit ta chưa làm gì cả.
    "// MARK:" có tác dụng giống macro #pragma mark trong Objective-C

    // MARK: LIFE CYCLE
    override init() {
        super.init();
    
        self.isFrontCamera = true
        self.session = AVCaptureSession()
        self.session.sessionPreset = AVCaptureSessionPreset640x480
    
    }
    
    deinit {}
    
  • Tiếp theo ta viết hàm yêu cầu quyền sử dụng camera của thiết bị (app sẽ hiện popup yêu cầu quyền truy cập camera)

    // MARK: INSTANCE METHODS
    
    func authorizeCamera(completionHandler: () -> Void) {
        AVCaptureDevice.requestAccessForMediaType(
            AVMediaTypeVideo,
            completionHandler: {
                (granted: Bool) -> Void in
                self.cameraGranted = granted
                // If permission hasn't been granted, notify the user.
                if !granted {
                    dispatch_async(dispatch_get_main_queue(), {
                        UIAlertView(
                            title: "Could not use camera!",
                            message: "This application does not have permission to use camera. Please update your privacy settings.",
                            delegate: self,
                            cancelButtonTitle: "OK").show()
                    })
                } else {
                    completionHandler()
                }
            }
        );
    }
    
  • Thêm lời gọi hàm authorizeCamera trong hàm khởi tạo, ngay dưới phần thiết lập sessionPreset

    self.isFrontCamera = true
    self.session = AVCaptureSession()
    self.session.sessionPreset = AVCaptureSessionPreset640x480
    self.authorizeCamera { () -> Void in
    //TODO
    }
    
  • Mở ViewController.swift. Khai báo biến

    var cameraSession: CameraSession!
    
  • Thêm hàm khởi tạo camera session

    func setupCameraView() {
    cameraSession = CameraSession()
    }
    
    override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.
    self.setupCameraView()
    }
    
  • Build và chạy thử ứng dụng :D

UI Interface

Bước 2: Khởi tạo camera input và output

  • Tiếp theo chúng ta sẽ viết các function khởi tạo và quản lý camera input và output

  • Hàm này sẽ tìm kiếm và trả về AVCaptureDevice Object

    class func deviceWithMediaType(mediaType: NSString, position: AVCaptureDevicePosition) -> AVCaptureDevice {
    var devices: NSArray = AVCaptureDevice.devicesWithMediaType(mediaType)
    var captureDevice: AVCaptureDevice = devices.firstObject as AVCaptureDevice
    for object:AnyObject in devices {
        let device = object as AVCaptureDevice
        if (device.position == position) {
            captureDevice = device
            break
        }
    }
    return captureDevice
    }
    
  • Thêm input từ camera trước

    func removeVideoInput() -> Bool {
    let currentVideoInputs:NSArray = self.session.inputs as NSArray;
    if (currentVideoInputs.count > 0) {
        self.session.removeInput(currentVideoInputs[0] as AVCaptureInput)
    }
    return true
    }
    // Setup camera input device (front facing camera) and add input feed to our AVCaptureSession session.
    func addFrontVideoInput() -> Bool {
    removeVideoInput()
    
    var success: Bool = false
    var error: NSError?
    
    var videoDevice: AVCaptureDevice = CameraSession.deviceWithMediaType(AVMediaTypeVideo, position: AVCaptureDevicePosition.Front)
    
    self.videoDeviceInput = AVCaptureDeviceInput.deviceInputWithDevice(videoDevice, error: &error) as AVCaptureDeviceInput;
    if (error == nil) {
        if self.session.canAddInput(self.videoDeviceInput) {
            self.session.addInput(self.videoDeviceInput)
            success = true
            isFrontCamera = true
        }
    }
    
    return success
    }
    
  • Thêm camera output

    func removeVideoOutput() -> Bool {
    let currentVideoOutputs:NSArray = self.session.outputs as NSArray;
    if (currentVideoOutputs.count > 0) {
        self.session.removeOutput(currentVideoOutputs[0] as AVCaptureOutput)
    }
    
    return true
    }
    // Setup capture output for our video device input.
    func addVideoOutput() {
    var settings: [String: Int] = [
        kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA
    ]
    
    self.videoDeviceOutput = AVCaptureVideoDataOutput()
    self.videoDeviceOutput.videoSettings = settings
    self.videoDeviceOutput.alwaysDiscardsLateVideoFrames = true
    
    self.videoDeviceOutput.setSampleBufferDelegate(self, queue: self.sessionQueue)
    
    if self.session.canAddOutput(self.videoDeviceOutput) {
        self.session.addOutput(self.videoDeviceOutput)
    }
    }
    func addStillImageOutput() {
    self.stillImageOutput = AVCaptureStillImageOutput()
    self.stillImageOutput.outputSettings = [AVVideoCodecKey: AVVideoCodecJPEG]
    
    if self.session.canAddOutput(self.stillImageOutput) {
        self.session.addOutput(self.stillImageOutput)
    }
    }
    
  • Hàm start/stop camera session

    func startCamera() {
    dispatch_async(self.sessionQueue, {
        self.runtimeErrorHandlingObserver = NSNotificationCenter.defaultCenter().addObserverForName(AVCaptureSessionRuntimeErrorNotification, object: self.sessionQueue, queue: nil, usingBlock: {
            [unowned self] (note: NSNotification!) -> Void in
            dispatch_async(self.sessionQueue, {
                self.session.startRunning()
            })
        })
        self.session.startRunning()
    })
    }
    func teardownCamera() {
    dispatch_async(self.sessionQueue, {
        self.session.stopRunning()
        NSNotificationCenter.defaultCenter().removeObserver(self.runtimeErrorHandlingObserver!)
    })
    }
    

Bước 3: Khai báo custom protocol

  • Khai báo custom protocol, chứa delegate thông báo sau khi camera session được khởi tạo thành công. Khai báo dưới phần import
@objc protocol CameraSessionDelegate {
    optional func cameraSessionDidOutputSampleBuffer(sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!)
    optional func capturingImage()
    optional func capturedImage()
    optional func cameraSessionDidReady()
}
  • Khai báo biến delegate object:

    var sessionDelegate: CameraSessionDelegate?
    
  • Trong hàm init, thêm vào block TODO sau khi gọi authorizeCamera function như sau:

    self.authorizeCamera { [unowned self] () -> Void in
    self.sessionQueue = dispatch_queue_create("CameraSessionController Session", DISPATCH_QUEUE_SERIAL)
    
    dispatch_async(self.sessionQueue, {
        self.session.beginConfiguration()
        self.addFrontVideoInput()
        self.addVideoOutput()
        self.addStillImageOutput()
        self.session.commitConfiguration()
        self.sessionDelegate?.cameraSessionDidReady?()
    })
    };
    
  • Giải thích một số điểm quan trọng:

    • sessionQueue: là background queue kiểu tuần tự (serial queue), tức là dữ liệu ảnh từ camera sẽ được xếp vào hàng đợi này và được xử lý kiểu first in - first out, tại một thời điểm chỉ có một task được xử lý. Tham khảo tại đây
    • Đầu ra của camera sẽ được truyền vào delegate của Object AVCaptureVideoDataOutput. Delegate này sẽ được gọi và chạy trên queue mà mình chỉ định thông qua command này: self.videoDeviceOutput.setSampleBufferDelegate(self, queue: self.sessionQueue)
    • self.videoDeviceOutput.alwaysDiscardsLateVideoFrames = true -> Nếu một video frame đang được xử lý trong delegate block thì những video frame tiếp sau sẽ bị loại bỏ ngay lập tức. Gần giống với khái niệm frame skipping trong game.
    • Các thao tác config và quản lý session sẽ được thực hiện trong hàng đợi để đảm bảo việc xử lý ảnh không bị ngắt quãng giữa chừng.
    • Tuy nhiên, nếu thực hiện việc xử lý nhận dạng ảnh song song với thao tác chụp ảnh tĩnh thì cần thực hiện song song để đảm bảo việc chụp ảnh tĩnh không bị delay bởi task xử lý nhận dạng.
    • Liên hệ tới self trong block thì nên sử dụng capture list [unowned self] để tránh self retain. Tham khảo tại đây, phần Capture List

Bước 4: Hiển thị previewLayer của camera

  • Tiếp tục với ViewController.swift. Đầu tiên phải tạo kết nối giữa giao diện hiển thị video mà ta đã tạo từ bài trước vào code.
  • Mở Main.storyboard => Bật assistant editor Assistant
  • Click và giữ chuột phải vào UIView hiển thị camera và kéo vào phần khai báo biến trong code. Nhập tên biến là previewView và ấn connect. IB IB
  • Khai báo biến: var previewLayer : AVCaptureVideoPreviewLayer!
  • Implement CameraSessionDelegate:
class ViewController: UIViewController, CameraSessionDelegate 
{ 
...
}
  • Update hàm setupCameraView:
func setupCameraView() {
    cameraSession = CameraSession()
    cameraSession.sessionDelegate = self
    self.previewLayer = AVCaptureVideoPreviewLayer(session: self.cameraSession.session)
    self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
    self.previewView.layer.addSublayer(self.previewLayer)
    updatePreviewLayerFrame()
}
func updatePreviewLayerFrame() {
    previewLayer.frame = self.previewView.layer.bounds
}

// MARK: CAMERA SESSION DELEGATE
func cameraSessionDidReady() {
    cameraSession.startCamera()
}
  • Yeah, xong một phần khá dài. Build ứng dụng để xem camera nào :D

Bước 5: Nhận diện khuôn mặt

Tiếp theo là ta sẽ code chức năng nhận diện khuôn mặt và gắn râu lên miệng.

  • Tạo file class FaceDetector.swift trong thư mục Helpers, nội dung như sau:
//
//  FaceDetector.swift
//  GoatCamera
//
//  Created by Hoang Pham Huu on 2/4/15.
//  Copyright (c) 2015 thucdon24. All rights reserved.
//

import UIKit
import CoreImage

private let _sharedCIDetector = CIDetector(
    ofType: CIDetectorTypeFace,
    context: nil,
    options: [
        CIDetectorAccuracy: CIDetectorAccuracyLow,
        CIDetectorTracking: false,
        CIDetectorMinFeatureSize: NSNumber(float: 0.1)
    ])

class FaceDetector {

    class var sharedCIDetector: CIDetector {
        return _sharedCIDetector
    }

    class func detectFaces(inImage image: CIImage) -> [CIFaceFeature] {
        let detector = FaceDetector.sharedCIDetector
            let features = detector.featuresInImage(
            image,
            options: [
                CIDetectorImageOrientation: 1,
                CIDetectorEyeBlink: false,
                CIDetectorSmile: false
            ])

        return features as [CIFaceFeature]
    }
}
  • Class FaceDetector được implement kiểu singleton. CIDetector được khởi tạo ở global scope. Hàm detectFaces là static function sử dụng đầu vào là ảnh CoreImage từ camera và output ra CIFaceFeature Object.

Mô tả cách gắn râu:

  • Hình ảnh camera được hiển thị ở previewLayer.
  • Ta sẽ tạo một layer mới và add đè lên previewLayer. Gọi là stickerLayer
  • Ảnh râu (mustacheLayer) sẽ được thêm vào stickerLayer.
  • stickerLayer sẽ có kích thước bằng với kích thước của ảnh output ra từ camera (Chính là kích thước 640*480 khi ta set sessionPreset của camera là AVCaptureSessionPreset640x480). Chọn như vậy là do ta sẽ nhận diện và xác định vị trí miệng trên ảnh có kích thước 640*480. Do đó có thể dùng luôn toạ độ nhận được để thêm râu lên stickerLayer.
  • Để hiển thị râu khớp với previewLayer, ta chỉ cần scale stickerLayer cho khớp với previewLayer.

Bắt đầu thực hiện:

  • Quay lại ViewController.swift. Khai báo thêm biến như sau:

    var stickerLayer : CALayer?
    var mustacheLayer: CALayer?
    let mustacheImage: UIImage? = UIImage(named: "mustache")
    
  • Trong CameraSession class, viết delegate lấy camera output:

    func captureOutput(captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!) {
    self.sessionDelegate?.cameraSessionDidOutputSampleBuffer?(sampleBuffer, fromConnection:connection)
    }
    
  • Trong ViewController, implement cameraSession delegate:

    func cameraSessionDidOutputSampleBuffer(sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!) {
    if (connection.supportsVideoOrientation) {
        connection.videoOrientation = AVCaptureVideoOrientation.Portrait
    }
    if (connection.supportsVideoMirroring) {
        if self.cameraSession.isFrontCamera == true {
            connection.videoMirrored = true
        }
    }
    updateStickerPosition(sampleBuffer)
    }
    
  • Khi nhận diện khuôn mặt, ta cần biết orientation của ảnh. Ở đây ta chỉ giới hạn xử lý Portrait để lược đi phần detect orientation của ảnh.

Tiếp tục implement hàm updateStickerPosition

func updateStickerPosition(sampleBuffer: CMSampleBuffer) {
    var pixelBuffer: CVImageBufferRef = CMSampleBufferGetImageBuffer(sampleBuffer)!
    var sourceImageColor: CIImage = CIImage(CVPixelBuffer: pixelBuffer)

    var width = sourceImageColor.extent().size.width
    var height = sourceImageColor.extent().size.height

    // Size of detection Image
    var cleanAperture:CGRect = CGRectMake(0, 0, CGFloat(width), CGFloat(height))

    let faceFeatures = FaceDetector.detectFaces(inImage: sourceImageColor)

    dispatch_async(dispatch_get_main_queue(), { [unowned self] () -> Void in
        self.drawStickers(faceFeatures, clearAperture: cleanAperture, orientation: UIDeviceOrientation.Portrait)
    })

}

Hàm drawSticker sẽ thực hiện nhiệm vụ draw và update position các layer

func drawStickers(features: NSArray, clearAperture: CGRect, orientation: UIDeviceOrientation) {
    var currentSublayer = 0
    var featuresCount = features.count
    var currentFeature = 0

    CATransaction.begin()
    CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions)

    if (featuresCount == 0) {
        stickerLayer?.hidden = true
        CATransaction.commit()
        return
    }

    var parentFrameSize = self.view.frame.size
    var gravity = self.previewLayer?.videoGravity

    // Take max scaleFactor
    var scaleFactorWidth = self.previewLayer.frame.width / clearAperture.width
    var scaleFactorHeight = self.previewLayer.frame.height / clearAperture.height
    var scaleFactor = scaleFactorHeight > scaleFactorWidth ? scaleFactorHeight : scaleFactorWidth

    for faceFeature in features {
        if !faceFeature.hasMouthPosition {
            continue
        }

        // Add new stickerLayer if not exist. Scale stickerLayer to fit previewLayer
        if (stickerLayer == nil) {
            stickerLayer = CALayer()
            stickerLayer?.frame = CGRectMake(0,
                0,
                clearAperture.width,
                clearAperture.height)
            stickerLayer?.position = self.previewLayer.position
            stickerLayer?.transform = CATransform3DMakeScale(scaleFactor, scaleFactor, 1)
            self.previewLayer.addSublayer(stickerLayer)
        }

        stickerLayer?.hidden = false

        // Add mustacheLayer into stickerLayer
        if (mustacheLayer == nil) {
            mustacheLayer = CALayer()
            mustacheLayer?.contents = self.mustacheImage?.CGImage
            //                mustacheLayer.borderColor = UIColor.redColor().CGColor
            //                mustacheLayer.borderWidth = 1
            self.stickerLayer?.addSublayer(mustacheLayer)
        }

        // Calculate mouthRect
        var faceRect = faceFeature.bounds

        faceRect = CGRectMake(0, 0, faceRect.width, faceRect.height)

        let mustacheWidth = faceRect.width / 2 * scaleFactor
        let mustacheHeight = mustacheWidth / mustacheImage!.size.width * mustacheImage!.size.height
        let mustacheSize = CGSize(
            width: mustacheWidth,
            height: mustacheHeight)


        let mustacheRect = CGRect(
            x: faceFeature.mouthPosition.x * scaleFactor - mustacheSize.width * 0.5 + 5,
            y: (clearAperture.height - faceFeature.mouthPosition.y) * scaleFactor - mustacheSize.height * 0.5 - 12,
            width: mustacheSize.width,
            height: mustacheSize.height)


        mustacheLayer?.frame = mustacheRect

        currentFeature++
    }

    CATransaction.commit()

}

Một số điểm cần lưu ý:

  • Sử dụng layer thay vì UIView để tăng performance
  • CALayer khi thay đổi vị trí, mặc định sẽ có Animation. Cần disable animation.
  • Toạ độ face feature sẽ theo trục toạ độ của OpenGL, y axis sẽ bị ngược so với UIKit. Bởi vậy khi tính toạ độ miệng cần lật ngược trục y.
  • Nhóm tất cả thao tác với Layer trong một CATTransaction để tối ưu performance.

Vậy là xong. Build và chạy thử. Bạn sẽ thấy một cái râu luôn gắn vào miệng :D

Bài này có vẻ dài quá, bản thân mình viết cũng thấy dài. Tóm tắt lại phần này, ta đã học được:

  • Cách khởi tạo camera session và sử dụng dispatch queue để thao tác với camera
  • Cách bắt dữ liệu sample liên tục từ camera và convert thành CIImage để nhận diện khuôn mặt.
  • Cách thao tác với CALayer để hiển thị râu trên camera previewLayer

Ở phần sau, ta sẽ hoàn thiện nốt phần render ra ảnh kèm râu trên miệng (chụp ảnh) và kỹ thuật resize, convert ảnh grayscale (Sử dụng CoreGraphic)

Toàn bộ code tutorial ở phần này các bạn có thể lấy về tại đây:
https://github.com/muzix/goatcamera
master chứa code ở phần 1. Checkout branch develop để xem code phần 2.

Đăng lại từ blog của tác giả (http://tech.thucdon24.com/swift-tutorial-a-simple-face-recognition-ios-app-part-2)

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

HoangPH

4 bài viết.
19 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
20 3
Hôm nay sẽ ngồi viết bài tutorial hoàn chỉnh đầu tiên :) Tại sao lại là chủ đề này, chả là vì trước Tết mình ngồi code một cái app chụp ảnh đồng t...
HoangPH viết hơn 3 năm trước
20 3
White
15 10
Giới thiệu 1. Stash Chắc hẳn mọi người ai cũng biết tới Github hay Bitbucket như là những dịch vụ lưu trữ , quản lý code và project hữu hiệu. Cả h...
HoangPH viết hơn 3 năm trước
15 10
White
11 4
(Link) (Link) (Link) Ở 2 phần tut trước, mình đã hướng dẫn khá chi tiết cách viết một ứng dụng camera có tích hợp chức năng nhận diện khuôn mặ...
HoangPH viết hơn 3 năm trước
11 4
Bài viết liên quan
White
11 4
(Link) (Link) (Link) Ở 2 phần tut trước, mình đã hướng dẫn khá chi tiết cách viết một ứng dụng camera có tích hợp chức năng nhận diện khuôn mặ...
HoangPH viết hơn 3 năm trước
11 4
Male avatar
0 0
RxSwift: Bài 6: RxCocoa (Part 4) Units ===== Updated ngày 30/06 Updated một chút: Vì những bất tiện và không rõ ràng về thông tin của kipalog, mì...
Bùi Khánh Duy viết 9 tháng trước
0 0
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
4 bài viết.
19 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!