Ở 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 640×480. 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 😀

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 editorAssistant

  • 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 😀

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 😀

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)

Comments

comments