Примечание: это перевод статьи рассказ ведется от лица автора, контакты, прямые ссылки и немного об авторе Вы сможете найти в конце статьи.
Недавно я немного поиграл с вышедшей для iOS библиотекой фотогалереи и камеры под названием Chafu. iOS обеспечивает достаточно простой API-интерфейс для того, чтобы можно было выполнять массу разнообразных задач, таких как чтение кодов bar и QR, а также некоторых других доступных для чтения машиной кодов. И ещё она поддерживает распознавание лиц!
Поскольку эта особенность забавная, я решил выяснить, как она работает, и продемонстрировать её вживую в предварительном просмотре на экране при съемке фото или записи видео, а затем добавить полученный результат в Chafu.
Важная информация: в iOS 7 добавлена функциональность AVFoundation, чтобы вышесказанное было возможным, поэтому то, что я здесь описываю, подходит для iOS 7 и выше, соответственно, имейте это в виду, если собираетесь проделать всё то же самое на более ранних версиях.
При подготовке этой статьи я исходил из того, что вы уже умеете устанавливать AVCaptureSession, AVVCaptureDeviceInput и AVCaptureVideoPreviewLayer для предпросмотра тех данных, которые выводятся из «камеры».
Настойка распознавания лиц
Сначала нужно добавить класс AVCaptureMetadataOutput к нашей сессии, его задачей является распознавание лиц и запросов, в нём должен быть реализован IAVCaptureMetadataOutputObjectsDelegate, из которого исходит обратный вызов при обнаружении лиц.
В установке AVCaptureMetadataOuput нет ничего сложного:
- Создайте этот экземпляр
- Добавьте его в AVCaptureSession
- Узнайте, доступны ли метаданные лиц
- Установите обратный вызов, если в пункте 3 получено положительное подтверждение.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | void SetupFaceDetection() { // create new AVCaptureMetadataOutput, this is what detects Faces faceDetectionOutput = new AVCaptureMetadataOutput(); // check if we can add it to the AVCaptureSession if (session.CanAddOutput(faceDetectionOutput)) { // most likely we can session.AddOutput(faceDetectionOutput); // check if face detection is available // AvailableMetadataObjectTypes is populated after AddOuput, it is empty before if (faceDetectionOutput.AvailableMetadataObjectTypes.HasFlag( AVMetadataObjectType.Face)) { // we want faces faceDetectionOutput.MetadataObjectTypes = AVMetadataObjectType.Face; // we want callbacks on Main Thread // (you decide here, just make sure UI changes are done on Main Thread) faceDetectionOutput.SetDelegate(this, DispatchQueue.MainQueue); } } } |
Заметили следующую вещь в обратном вызове? Это реализация IAVCaptureMetadataOutputObjectsDelegate. Я решил добавить его непосредственно в мой класс Camera. Реализация выглядит приблизительно следующим образом:
1 2 3 4 5 6 7 8 9 | public class MyCameraView : UIView, IAVCaptureMetadataOutputObjectsDelegate { [Export("captureOutput:didOutputMetadataObjects:fromConnection:")] public void DidOutputMetadataObjects(AVCaptureMetadataOutput captureOutput, AVMetadataObject[] metadataObjects, AVCaptureConnection connection) { // faces are dumped here... } } |
Экспорт является очень важным, поскольку за счёт него мы сообщаем миру iOS то, что мы осуществили реализацию интерфейса, именно так наш код сохраняется.
Сейчас просто надеемся на лучшее. При выполнении этого кода и установлении места разрыва в методе DidOutputMetadataObjects, он станет срабатывать в момент обнаружения лица. Аргумент metadataObjects будет содержать AVMetadataFaceObjects, который в свою очередь включает в себя всё, что Вам необходимо для визуальной демонстрации обнаруженных лиц.
Отображение лиц
Для отображения лиц мне нравится использовать уровни иерархической структуры iOS (layers), которые позволяют нам с лёгкостью осуществлять повороты и другие преобразования. Я чуть позже объясню, как использовать Roll и Yaw из AVMetadataFaceObject при отрисовке визуального индикатора лица.
Сперва нам нужно установить CALayer, в который мы будем добавлять распознавание лиц. Это очень просто и также в дальнейшем будет довольно легко очищать подуровни, когда нам потребуется сделать так, чтобы лица перестали демонстрироваться.
1 2 | overlayLayer = new CALayer { Frame = Bounds }; videoPreviewLayer.AddSublayer(overlayLayer); |
Теперь мы можем добавить «лица» к этому уровню в методе DidOutputMetadataObjects. И это делается всего в несколько простых шагов.
- Выполните итерацию массива metadataObjects, который мы получаем в качестве аргумента
- Убедитесь в том, что это на самом деле AVMetadataFaceObject
- Преобразуйте через AVCaptureVideoPreviewLayer объект GetTransformedMetadataObject, чтобы получить правильные координаты для лица
- Создайте для лица новый CALayer с рамкой вокруг объекта или другим желаемым эффектом
- Установите привязки CALayer с тем, что получилось от преобразования
- Добавьте это в качестве подуровня в overlayLayer, который мы создали ранее
Давайте начнем с определения метода для того, как будет выглядеть уровень, демонстрирующий лицо.
1 2 3 4 5 6 7 8 9 10 11 | CALayer CreateFaceLayer() { var faceLayer = new CALayer { BorderColor = UIColor.White.CGColor, BorderWidth = 2, CornerRadius = 5 }; return faceLayer; } |
Здесь я просто создаю новый CALayer и устанавливаю цвет границы, ширину и радиус скругления углов. Итак, в этом случае мы увидим белые квадраты со второй (2) шириной границы и закругленными углами. Всё просто!
Теперь, давайте добавим этот уровень к overlayLayer, и, таким образом, мы уже на самом деле сможем увидеть что-то на экране.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | [Export("captureOutput:didOutputMetadataObjects:fromConnection:")] public void DidOutputMetadataObjects(AVCaptureMetadataOutput captureOutput, AVMetadataObject[] metadataObjects, AVCaptureConnection connection) { // only interested in faces foreach (var metadata in metadataObjects.OfType<AVMetadataFaceObject>()) { // transform the metadata object to adjust bounds var transformed = VideoPreviewLayer.GetTransformedMetadataObject(metadata); // used later, from this we will get yaw and roll angles var face = (AVMetadataFaceObject)transformed; // create a new layer var faceLayer = CreateFaceLayer(); // set the transformed bounds faceLayer.Frame = transformed.Bounds; // display it! overlayLayer.AddSublayer(faceLayer); } } |
Вот и всё! Сейчас уже должны появиться определённые квадраты, предназначенные для отображения лиц. Между тем есть одна проблема. При таком раскладе может добавиться целая куча уровней. Поэтому нам нужно удалить подуровни, прежде чем добавлять новые.
1 2 3 4 5 | void RemoveFaces() { foreach(var layer in overlayLayer.Sublayers) layer.RemoveFromSuperLayer(); } |
Вызовите метод RemoveFaces(), прежде чем вы выполните итерацию metadataObjects в DidOutputMetadataObjects и всё должно получиться.
Внимательный читатель должен, наверное, заметить, что это кажется неэффективным. Я не буду подробно останавливаться на этом в данной статье. Тем не менее Вы можете увидеть один возможный вариант решения этой проблемы: у BaseCameraView в Chafu там, где я сохраняю отслеживание FaceId от AVMetadataFaceObject, можно просто настроить границы для этого лица, если оно переместилось.
Выравнивание для углов Yaw и Roll
AVMetadataFaceObject выдает нам RollAngle при повороте шапки вокруг оси Z. Он также дает нам YawAngle для вращений вокруг оси Y.
Поддержка RollAngle является простой, так как требуется проводить вращение не в Z плоскости, а скорее вокруг нее. Однако, вращения вокруг оси Y, будут перемещать прямоугольник в Z плоскости. По умолчанию CALayer является плоским и не имеет даже малейшей перспективы. Мы можем исправить это! Вернувшись туда, где мы создаем overlayLayer, нам нужно добавить простое преобразование, которое, получается, и даст эту перспективу.
1 2 3 4 5 6 7 8 | var transform = CATransform3d.Identity; transform.m34 = -1.0f / 1000; overlayLayer = new CALayer { Frame = Bounds, SublayerTransform = transform }; videoPreviewLayer.AddSublayer(overlayLayer); |
Благодаря этому будет взято преобразование по умолчанию и добавлено расстояние до плоскости проекции 3D в условиях 1/z. Другими словами, мы добавили глубину. Apple делает это в обратном порядке. Следовательно, используется -1/z, где z есть расстояние, в этом случае оно 1000. Чем больше значение, тем больше расстояние. Для более подробного толкования вы можете обратиться к статье 3D-проекции на Wikipedia.
Теперь мы можем применить наши повороты к лицу CALayers.
Roll
Для того чтобы заставить Roll вращаться мы просто создаем новый CATransform3D, используя статический метод MakeRotation, который позволяет производить вращения вокруг любой оси. Как было показано выше roll вращается вокруг оси Z. CATransform3D ожидает того, что угол в радианах. Следовательно, нам нужно преобразовать это в первую очередь.
1 2 3 4 5 6 7 8 | CATransform3D RollTransform(nfloat rollAngle) { // convert angle to radians var radians = rollAngle * (nfloat)Math.PI / 180f; // rotate around Z axis return CATransform3D.MakeRotation(radians, 0.0f, 0.0f, 1.0f); } |
Это довольно просто. Мы применим это к уровню лица позже.
Yaw
Вращение Yaw немного более трудоёмкий процесс. Нам нужно знать об ориентации устройства, поскольку ось Y изменяется в зависимости от ориентации, и для этого нам нужно отрегулировать угол. Это означает, что лица всегда распознаются в той же ориентации. Тем не менее наш предварительный уровень будет изменяться на основании ориентации.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | private static CATransform3D OrientationTransform() { // figure out the orientation of the device and adjust rotation nfloat angle = 0.0f; switch (UIDevice.CurrentDevice.Orientation) { case UIDeviceOrientation.PortraitUpsideDown: angle = (nfloat)Math.PI; break; case UIDeviceOrientation.LandscapeRight: angle = (nfloat)(-Math.PI / 2.0f); break; case UIDeviceOrientation.LandscapeLeft: angle = (nfloat)Math.PI / 2.0f; break; default: angle = 0.0f; break; } return CATransform3D.MakeRotation(angle, 0.0f, 0.0f, 1.0f); } |
Теперь нам нужно сделать преобразование Yaw и объединить его с трансформацией ориентации.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | CATransform3D YawTransform(nfloat yawAngle) { // convert angle to radians var radians = yawAngle * (nfloat)Math.PI / 180f; // rotate around Y axis var yawTransform = CATransform3D.MakeRotation(radians, 0.0f, -1.0f, 0.0f); // get transformation for current orientation var orientationTransform = OrientationTransform(); // concat the two transforms return orientationTransform.Concat(yawTransform); } |
И, наконец, применить эти два преобразования к лицу CALayer в методе DidOutputMetadataObjects.
1 2 3 4 5 6 7 8 9 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 | [Export("captureOutput:didOutputMetadataObjects:fromConnection:")] public void DidOutputMetadataObjects(AVCaptureMetadataOutput captureOutput, AVMetadataObject[] metadataObjects, AVCaptureConnection connection) { RemoveFaces(); // only interested in faces foreach (var metadata in metadataObjects.OfType<AVMetadataFaceObject>()) { // transform the metadata object to adjust bounds var transformed = VideoPreviewLayer.GetTransformedMetadataObject(metadata); // used later, from this we will get yaw and roll angles var face = (AVMetadataFaceObject)transformed; // create a new layer var faceLayer = CreateFaceLayer(); // set the transformed bounds faceLayer.Frame = transformed.Bounds; faceLayer.Transform = CATransform3D.Identity; if (face.HasRollAngle) { var transform = RollTransform(face.RollAngle); faceLayer.Transform = faceLayer.Transform.Concat(transform); } if (face.HasYawAngle) { var transform = YawTransform(face.YawAngle); faceLayer.Transform = faceLayer.Transform.Concat(transform); } // display it! overlayLayer.AddSublayer(faceLayer); } } |
Заметьте, я добавил преобразование по умолчанию к faceLayer и сочленил roll- и/или yaw-преобразования в зависимости от их доступности. Вот и всё. Теперь у Вас должно получиться что-то вроде этого. Скриншоты взяты из Chafu, и на них демонстрируется распознавание, при котором не выполнялось вращение, а потом показаны варианты с roll и yaw.
Автор: Tomasz Cielecki (Cheesebaron)
Источник: Официальный блог автора
Twitter: @Cheesebaron
GitHub: Cheesebaron
Немного об авторе:
Имеет статус Xamarin MVP и является членом сообщества Xamarin.
Написать ответ