Я делаю нативное приложение для 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 . |