Я делаю нативное приложение для 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 Foundationimport UIKitclass 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 Foundationimport AVFoundationimport UIKitimport CoreMotionimport ImageIOclass 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 Foundationimport AVFoundationclass 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 Foundationimport CoreImageimport AVFoundationclass 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 . |
