Статьи

В твое лицо!

Я делаю нативное приложение для iOS с распознаванием лиц. У Apple есть потрясающий API для обнаружения изображений, который может находить лица, штрих-коды и даже прямоугольные формы в изображениях или видеокадрах. API вышел с iOS 5.0, но я подумал, что обновленный пример с Swift 2.2 и Xcode 7.3, надеюсь, поможет людям.

Код, расположенный по адресу https://github.com/dcandre/face-it , позволит вам просматривать видеопоток с камеры вашего устройства iOS и накладывать файл раскадровки поверх предварительного просмотра в положениях левого и правого глаза. ,

Дерек

Я собираюсь предположить, что вы можете создать приложение Single View в XCode. Мой код ограничивает ориентацию устройства в портретном режиме. Я создал группу в Project Navigator под названием Video-Capture . В этой группе вы можете создать файл VideoCaptureController.swift .

Класс VideoCaptureController

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import Foundation
import UIKit
 
class VideoCaptureController: UIViewController {
    var videoCapture: VideoCapture?
     
    override func viewDidLoad() {
        videoCapture = VideoCapture()
    }
     
    override func didReceiveMemoryWarning() {
        stopCapturing()
    }
     
    func startCapturing() {
        do {
            try videoCapture!.startCapturing(self.view)
        }
        catch {
            // Error
        }
    }
     
    func stopCapturing() {
        videoCapture!.stopCapturing()
    }
     
    @IBAction func touchDown(sender: AnyObject) {
        let button = sender as! UIButton
        button.setTitle("Stop", forState: UIControlState.Normal)
         
        startCapturing()
    }
     
    @IBAction func touchUp(sender: AnyObject) {
        let button = sender as! UIButton
        button.setTitle("Start", forState: UIControlState.Normal)
         
        stopCapturing()
    }   
}

Магия, которая захватывает видео и выполняет распознавание лиц, будет заключена в класс VideoCapture , который мы создадим дальше. Пока что мы предположим, что интерфейс для класса VideoCapture будет иметь два метода startCapturing и stopCapturing . Обратите внимание на два метода действия. Когда пользователь нажимает кнопку, начинается захват видео, а когда он поднимается на кнопку, захват видео останавливается. Как Snapchat, Instragram, Vine или другие приложения для захвата видео. Вы можете проверить раскадровку в моем коде, но не стесняйтесь создавать свой собственный интерфейс для запуска и остановки захвата видео.

viewDidLoad и didReceiveMemoryWarning из класса UIViewController перезаписываются. Они будут использоваться для создания экземпляра нашего объекта видеозахвата и предотвращения его захвата, если у нас есть предупреждения памяти.

Зайдите в раскадровку и выберите свой контроллер представления. В Identity Inspector измените пользовательский класс на свой файл VideoCaptureController . Я использовал события «Touch Down», «Touch Up Inside» и «Touch Up Outside», чтобы присоединить методы действий контроллера представления.

Прежде чем говорить о классе VideoCapture, я хочу подвести итог процесса захвата видео Apple. Для захвата изображений или видео с камеры вашего устройства iOS вы используете платформу AVFoundation. Класс AVCaptureSession связывает входы, как камера, и выходы, как сохранение в файл изображения. Мы будем использовать вывод с именем AVCaptureVideoDataOutput . Это захватит кадры из видео и позволит нам увидеть то, что видит камера.

Создайте файл в группе VideoCapture с именем VideoCapture.swift . Завершенный класс VideoCapture можно найти на GitHub . Вот объявление класса:

Класс VideoCapture

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import Foundation
import AVFoundation
import UIKit
import CoreMotion
import ImageIO
 
class VideoCapture: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
    var isCapturing: Bool = false
    var session: AVCaptureSession?
    var device: AVCaptureDevice?
    var input: AVCaptureInput?
    var preview: CALayer?
    var faceDetector: FaceDetector?
    var dataOutput: AVCaptureVideoDataOutput?
    var dataOutputQueue: dispatch_queue_t?
    var previewView: UIView?
     
    enum VideoCaptureError: ErrorType {
        case SessionPresetNotAvailable
        case InputDeviceNotAvailable
        case InputCouldNotBeAddedToSession
        case DataOutputCouldNotBeAddedToSession
    }
     
    override init() {
        super.init()
         
        device = VideoCaptureDevice.create()
         
        faceDetector = FaceDetector()
    }
     
    func startCapturing(previewView: UIView) throws {
        isCapturing = true
         
        self.previewView = previewView
         
        self.session = AVCaptureSession()
         
        try setSessionPreset()
         
        try setDeviceInput()
         
        try addInputToSession()
         
        setDataOutput()
         
        try addDataOutputToSession()
         
        addPreviewToView(self.previewView!)
         
        session!.startRunning()
    }
     
    func stopCapturing() {
        isCapturing = false
         
        stopSession()
         
        removePreviewFromView()
         
        removeFeatureViews()
         
        preview = nil
        dataOutput = nil
        dataOutputQueue = nil
        session = nil
        previewView = nil
    }
}

Этот класс должен наследоваться от NSObject , потому что протокол AVCaptureVideoDataOutputSampleBufferDelegate наследуется от NSObjectProtocol . NSObject заботится о реализации NSObjectProtocol . Мы можем поговорить о реализации captureOutput для AVCaptureVideoDataOutputSampleBufferDelegate позже.

Я создал перечисление, чтобы пользователи этого класса могли ловить определенные ошибки. Я расскажу об объектах device и faceDetector позже в перезаписанном методе init. Я создал методы startCapturing и stopCapturing , но мы не реализовали все методы, которые они вызывают. Мы рассмотрим их все, а затем реализуем VideoCaptureDevice и FaceDetector .

сессия

Если вы хотите захватить видео или изображение с камеры устройства iOS, вам нужно создать экземпляр класса AVCaptureSession . Мы назначаем переменную session в методе startCapturing . Затем мы вызываем метод setSessionPreset . Это должно быть добавлено в ваш класс VideoCapture.

1
2
3
4
5
6
7
8
private func setSessionPreset() throws {
    if (session!.canSetSessionPreset(AVCaptureSessionPreset640x480)) {
        session!.sessionPreset = AVCaptureSessionPreset640x480
    }
    else {
        throw VideoCaptureError.SessionPresetNotAvailable
    }
}

Это проверяет, может ли камера захватывать видео с разрешением 640 × 480. Если нет, то он выдаст ошибку. Я использую разрешение 640 × 480, но вы можете использовать другие разрешения. Вот их список . Теперь, когда у нас есть объект AVCaptureSession, мы можем начать добавлять входные и выходные классы.

Устройство ввода

Мы собираемся добавить две функции в наш класс VideoCapture. Метод setDeviceInput создает экземпляр класса AVCaptureDeviceInput . Это будет обрабатывать порты устройства ввода и позволит вам использовать камеру на вашем устройстве iOS.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
private func setDeviceInput() throws {
    do {
        self.input = try AVCaptureDeviceInput(device: self.device)
    }
    catch {
        throw VideoCaptureError.InputDeviceNotAvailable
    }
}
 
private func addInputToSession() throws {
    if (session!.canAddInput(self.input)) {
        session!.addInput(self.input)
    }
    else {
        throw VideoCaptureError.InputCouldNotBeAddedToSession
    }
}

Вывод данных

У нас есть сеанс и классы ввода, но теперь мы хотим захватить видеокадры для обнаружения лица Мы добавим еще один метод в класс VideoCapture.

01
02
03
04
05
06
07
08
09
10
11
12
13
private func setDataOutput() {
    self.dataOutput = AVCaptureVideoDataOutput()
     
    var videoSettings = [NSObject : AnyObject]()
    videoSettings[kCVPixelBufferPixelFormatTypeKey] = Int(CInt(kCVPixelFormatType_32BGRA))
     
    self.dataOutput!.videoSettings = videoSettings
    self.dataOutput!.alwaysDiscardsLateVideoFrames = true
     
    self.dataOutputQueue = dispatch_queue_create("VideoDataOutputQueue", DISPATCH_QUEUE_SERIAL)
     
    self.dataOutput!.setSampleBufferDelegate(self, queue: self.dataOutputQueue!)
}

Класс AVCaptureVideoDataOutput позволит нам обрабатывать несжатые кадры из нашего видеопотока. Свойство videoSettings представляет собой словарь с одной парой ключ / значение. kCVPixelBufferPixelFormatTypeKey — тип формата, в котором должны быть возвращены видеокадры. Это четырехсимвольный код, который преобразуется в целое число для класса AVCaptureVideoDataOutput .

Как вы можете себе представить, камера будет производить много видеокадров. Может быть, даже 60 / секунду. Вот почему мы используем метод dispatch_queue_create для создания последовательной очереди с Grand Central Dispatch. Этот тип очереди будет обрабатывать один запрос за раз в том порядке, в котором они были добавлены в очередь.

Наконец, для очереди создается образец буфера. Если мы потратим слишком много времени на обработку кадров, запрос будет удален из очереди, поскольку мы пометили свойство alwaysDiscardsLateVideoFrames как true.

Затем добавьте этот метод в ваш класс VideoCapture.

1
2
3
4
5
6
7
8
private func addDataOutputToSession() throws {
    if (self.session!.canAddOutput(self.dataOutput!)) {
        self.session!.addOutput(self.dataOutput!)
    }
    else {
        throw VideoCaptureError.DataOutputCouldNotBeAddedToSession
    }
}

Это добавит класс AVCaptureVideoDataOutput к классу AVCaptureSession.

Видеть значит верить

Добавьте следующий метод в ваш класс VideoCapture.

1
2
3
4
5
6
private func addPreviewToView(view: UIView) {
    self.preview = AVCaptureVideoPreviewLayer(session: session!)
    self.preview!.frame = view.bounds
     
    view.layer.addSublayer(self.preview!)
}

Мы собираемся создать экземпляр класса AVCaptureVideoPreviewLayer . Этот класс позволит вам увидеть видеокадры с устройства ввода. Затем мы добавим это в качестве подслоя представления, которое мы передаем из VideoCaptureController. В нашем примере это основной UIView, связанный с этим контроллером. Вы заметите, что мы устанавливаем рамку слоя в границах вмещающего вида. По сути, это полный размер представления.

Если вы проверите метод startCapturing в классе VideoCapture, вы увидите, что все методы на месте. Мы точно еще не закончили. Как мы получаем видеокадры из очереди и как мы на самом деле обнаруживаем лица в этих кадрах?

AVCaptureVideoDataOutputSampleBufferDelegate Protocol

Для протокола AVCaptureVideoDataOutputSampleBufferDelegate требуется только один метод. Это captureOutput .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
func captureOutput(captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!) {
         
    let image = getImageFromBuffer(sampleBuffer)
     
    let features = getFacialFeaturesFromImage(image)
     
    let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer)
     
    let cleanAperture = CMVideoFormatDescriptionGetCleanAperture(formatDescription!, false)
     
    dispatch_async(dispatch_get_main_queue()) {
        self.alterPreview(features, cleanAperture: cleanAperture)
    }
}

Мы можем получить видеокадр из CMSampleBuffer, который передается этой функции. Мы получаем некоторые свойства изображения и затем отправляем асинхронный запрос через Grand Central Dispatch . Мы используем основной поток, dispatch_get_main_queue . Вы хотите использовать основной поток при обновлении пользовательского интерфейса приложения, поскольку другие запросы не будут выполняться до вашего запроса, что приведет к ошибкам.

Давайте добавим функцию getImageFromBuffer в ваш класс VideoCapture.

1
2
3
4
5
6
7
8
9
private func getImageFromBuffer(buffer: CMSampleBuffer) -> CIImage {
    let pixelBuffer = CMSampleBufferGetImageBuffer(buffer)
     
    let attachments = CMCopyDictionaryOfAttachments(kCFAllocatorDefault, buffer, kCMAttachmentMode_ShouldPropagate)
     
    let image = CIImage(CVPixelBuffer: pixelBuffer!, options: attachments as? [String : AnyObject])
     
    return image
}

CMSampleBufferGetImageBuffer вернет буфер изображения. Словарь attachments заполняется CMCopyDictionaryOfAttachments , который копирует все свойства объекта sampleBuffer . Базовый объект Image возвращается.

Идите дальше и добавьте метод getFacialFeaturesFromImage в ваш класс VideoCapture. Код обнаружения лица инкапсулирован в классе FaceDetector. Мы вернемся к этому позже.

1
2
3
4
5
private func getFacialFeaturesFromImage(image: CIImage) -> [CIFeature] {
    let imageOptions = [CIDetectorImageOrientation : 6]
     
    return self.faceDetector!.getFacialFeaturesFromImage(image, options: imageOptions)
}

Я установил ориентацию для определения портрета, которая равна 6, так как это приложение заблокировано в портретном режиме. Метод getFacialFeaturesFromImage класса FaceDetector возвращает массив объектов CIFeature . В нашем случае они будут подклассом CIFaceFeature . Этот объект может сказать вам, видны ли глаза и рот и где они расположены. Он даже скажет вам, если обнаружит улыбку. Прежде чем мы создадим класс FaceDetector, давайте посмотрим на метод alterPreview который мы асинхронно отправляем для взаимодействия с пользовательским интерфейсом.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
private func alterPreview(features: [CIFeature], cleanAperture: CGRect) {
    removeFeatureViews()
     
    if (features.count == 0 || cleanAperture == CGRect.zero || !isCapturing) {
        return
    }
     
    for feature in features {
        let faceFeature = feature as? CIFaceFeature
         
        if (faceFeature!.hasLeftEyePosition) {
             
            addEyeViewToPreview(faceFeature!.leftEyePosition.x, yPosition: faceFeature!.leftEyePosition.y, cleanAperture: cleanAperture)
        }
         
        if (faceFeature!.hasRightEyePosition) {
             
            addEyeViewToPreview(faceFeature!.rightEyePosition.x, yPosition: faceFeature!.rightEyePosition.y, cleanAperture: cleanAperture)
        }
         
    }
     
}
 
private func removeFeatureViews() {
    if let pv = previewView {
        for view in pv.subviews {
            if (view.tag == 1001) {
                view.removeFromSuperview()
            }
        }
    }
}
 
private func addEyeViewToPreview(xPosition: CGFloat, yPosition: CGFloat, cleanAperture: CGRect) {
    let eyeView = getFeatureView()
    let isMirrored = preview!.contentsAreFlipped()
    let previewBox = preview!.frame
     
    previewView!.addSubview(eyeView)
     
    var eyeFrame = transformFacialFeaturePosition(xPosition, yPosition: yPosition, videoRect: cleanAperture, previewRect: previewBox, isMirrored: isMirrored)
     
    eyeFrame.origin.x -= 37
    eyeFrame.origin.y -= 37
     
    eyeView.frame = eyeFrame
}

В методе alterPreview мы удаляем виды, которые мы alterPreview над глазами, потому что мы перемещаем их в каждом кадре. Если не было найдено никаких черт лица, то мы будем спасаться, ничего не делая с рамой. Если левый или правый глаз найден, то мы вызываем метод addEyeViewToPreview(xPosition . Этот метод содержит несколько методов, которые нам также необходимо добавить в наш класс VideoCapture. getFeatureView загрузит файл Storyboard, который я назвал HeartView в мой проект.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private func getFeatureView() -> UIView {
    let heartView = NSBundle.mainBundle().loadNibNamed("HeartView", owner: self, options: nil)[0] as? UIView
    heartView!.backgroundColor = UIColor.clearColor()
    heartView!.layer.removeAllAnimations()
    heartView!.tag = 1001
     
    return heartView!
}
 
private func transformFacialFeaturePosition(xPosition: CGFloat, yPosition: CGFloat, videoRect: CGRect, previewRect: CGRect, isMirrored: Bool) -> CGRect {
     
        var featureRect = CGRect(origin: CGPoint(x: xPosition, y: yPosition), size: CGSize(width: 0, height: 0))
        let widthScale = previewRect.size.width / videoRect.size.height
        let heightScale = previewRect.size.height / videoRect.size.width
         
        let transform = isMirrored ? CGAffineTransformMake(0, heightScale, -widthScale, 0, previewRect.size.width, 0) :
            CGAffineTransformMake(0, heightScale, widthScale, 0, 0, 0)
         
        featureRect = CGRectApplyAffineTransform(featureRect, transform)
         
        featureRect = CGRectOffset(featureRect, previewRect.origin.x, previewRect.origin.y)
         
        return featureRect
    }

Метод getFeatureView загружает файл XIB и помечает его целым числом 1001, чтобы мы могли вернуться позже и легко удалить его с removeFeatureViews метода removeFeatureViews . Метод transformFacialFeaturePosition использует CGRectApplyAffineTransform для преобразования координат из одной системы координат в другую. Почему мы должны делать то, что вы спрашиваете? Видео снимается с разрешением 640 × 480, но наш предварительный просмотр находится в портретном режиме с различной шириной и высотой, в зависимости от того, как представление отображается в соответствии с окном. Различные представления представлены объектами videoRect , videoRect и previewRect . Как только у нас есть объект CGRect, который представляет положения глаз в системе координат представления предварительного просмотра, мы можем присоединить их к previewView в качестве подпредставления.

Наш класс VideoCapture выглядит довольно хорошо. Мы можем закончить, создав два оставшихся класса: класс VideoCaptureDevice и класс FaceDetector.

Класс VideoCaptureDevice

Создайте новый файл с именем VideoCaptureDevice.swift в группе VideoCapture. Вот полный класс на GitHub . Теперь у нас есть объект устройства для метода setDeviceInput в классе VideoCapture.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
import Foundation
import AVFoundation
 
class VideoCaptureDevice {
     
    static func create() -> AVCaptureDevice {
        var device: AVCaptureDevice?
         
        AVCaptureDevice.devicesWithMediaType(AVMediaTypeVideo).forEach { videoDevice in
            if (videoDevice.position == AVCaptureDevicePosition.Front) {
                device = videoDevice as? AVCaptureDevice
            }
        }
         
        if (nil == device) {
            device = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
        }
         
        return device!
    }
}

Этот класс содержит статический метод фабрики, который создает экземпляр AVCaptureDevice . Это приложение использует видео, поэтому мы используем метод devicesWithMediaType чтобы найти массив устройств типа AVMediaTypeVideo . Так как мы делаем обнаружение лица, я подумал, что передняя камера будет идеальной. Если передняя камера не найдена, то метод defaultDeviceWithMediaType используется для возврата камеры, способной снимать видео, которая, скорее всего, будет задней камерой.

Класс FaceDetector

Добавьте файл с именем FaceDetector.swift в группу VideoCapture. Вот полный класс на GitHub .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
import Foundation
import CoreImage
import AVFoundation
 
class FaceDetector {
    var detector: CIDetector?
    var options: [String : AnyObject]?
    var context: CIContext?
     
    init() {
        context = CIContext()
         
        options = [String : AnyObject]()
        options![CIDetectorAccuracy] = CIDetectorAccuracyLow
         
        detector = CIDetector(ofType: CIDetectorTypeFace, context: context!, options: options!)
    }
     
    func getFacialFeaturesFromImage(image: CIImage, options: [String : AnyObject]) -> [CIFeature] {
        return self.detector!.featuresInImage(image, options: options)
    }
}

Класс CIDetector — это интерфейс, который мы будем использовать для обнаружения лиц в нашем объекте CIImage, который мы создали из sampleBuffer. Параметр CIDetectorTypeFace — это строка, которая сообщает классу CIDetector о необходимости поиска лиц. Одним из параметров для CIDetector является CIDetectorAccuracy . Мы установили его на низкое значение, чтобы не было проблем с производительностью в отношении количества кадров, которые мы собираемся обрабатывать.

Последние мысли

Помните, что в симуляторе iOS нет камеры, поэтому вам нужно протестировать приложение на реальном устройстве. Когда вы запустите приложение и начнете захват, вы должны увидеть файл раскадровки, наложенный на ваши глаза. В будущем я хотел бы улучшить этот код, чтобы он не создавал новые объекты UIView для каждого кадра. Он просто переместит существующие или удалит их после завершения захвата видео.

Вот и все! Дайте мне знать, если у вас есть какие-либо вопросы или комментарии!

Ссылка: В твое лицо! Изучение API Apple Face Detection API от нашего партнера JCG Дерека Андре в блоге Keyhole Software .