Flutter
Face SDK предоставляет плагин для Flutter, который позволяет реализовать следующие функции:
- детекция лиц на фото
- детекция лиц на видео
- проверка Active Liveness
- верификация лиц
Плагин разработан для iOS и Android устройств.
Демонстрационный Flutter сэмпл с плагином Face SDK доступен в директории examples/flutter/demo дистрибутива Face SDK.
Подключение плагина Face SDK к flutter-проекту
Требования
- Flutter 2.2 или выше
- Android Studio для Android или XCode для iOS
Подключение плагина
Для подключения Face SDK к flutter-проекту, компонент "flutter" должен быть установлен с помощью инсталлятора Face SDK или утилиты maintenancetool:
Если Face SDK не установлен, следуйте инструкции по установке Приступая к работе. Необходимо выбрать компонент "flutter" в разделе "Выбор компонентов".
Если Face SDK установлен без компонента "flutter" (директория flutter отсутствует в корневой директории Face SDK), воспользуйтесь утилитой maintenancetool и установите компонент "flutter", выбрав его в разделе "Выбор компонентов".
Добавить плагины Face SDK и Path Provider в зависимости проекта, указав их в файле <project_dir>/pubspec.yaml
face_sdk_3divi, указав путь до директории с плагином в поле
path
path_provider версии 2.0.0 или выше
dependencies:
flutter:
sdk: flutter
face_sdk_3divi:
path: ../flutter/face_sdk_3divi
path_provider: "^2.0.0"
Добавить библиотеку libfacerec.so в зависимости проекта
2.a. Для Android-устройств:
указать путь до директории с библиотекой libfacerec.so в блоке
sourceSets
файла build.gradle (<project_dir>/android/app/build.gradle)android {
sourceSets {
main {
jniLibs.srcDirs = ["${projectDir}/../../assets/lib"]
}
}
}указать загрузку native-библиотеки в MainActivity.java (<project_dir>/android/app/src/main/java/<android_app_name>/MainActivity.java):
public class MainActivity extends FlutterActivity {
static{ System.loadLibrary("facerec"); }
}
2.b. Для iOS-устройств:
- окрыть файл ios/Runner.xcworkspace в программе XCode
- в меню Target Navigator выбрать "Runner", далее перейти на вкладку "General", в секции "Frameworks, Libraries, and Embedded Content" нажать на кнопку "+". В открывшемся окне нажать "Add Other...", затем "Add Files" и выбрать facerec.framework в Finder
- удалить facerec.framework в секции "Link Binary With Libraries" на вкладке "Build Phases"
Добавить директории и файлы из дистрибутива Face SDK в ассеты приложения:
- Создать директорию <project_dir>/assets (если отсутствует)
- Скопировать директорию lib из директории flutter в <project_dir>/assets
- Скопировать необходимые файлы из директорий conf и share в <project_dir>/assets/conf и <project_dir>/assets/share
- Создать директорию <project_dir>/assets/license
- Скопировать файл лицензии 3divi_face_sdk.lic в директорию <project_dir>/assets/license
Указать список директорий и файлов в <project_dir>/pubspec.yaml, пример:
flutter
assets:
- assets/conf/facerec/
- assets/license/3divi_face_sdk.lic
- assets/share/face_quality/
- assets/share/faceanalysis/
- assets/share/facedetectors/blf/
- assets/share/facedetectors/uld/
- assets/share/facedetectors/config_lbf/
- assets/share/facedetectors/config_lbf_noise/
- assets/share/fda/
- assets/share/iris/
- assets/share/facerec/recognizers/method10v30/ПримечаниеFlutter не копирует директории рекурсивно, поэтому необходимо указывать каждую директорию с файлами.
Добавить в проект функцию копирования ассетов во внутреннюю память приложения (это требуется для корректной работы Face SDK).
late String dataDir;
Future<String> loadAsset() async {
final manifestContent = await rootBundle.loadString('AssetManifest.json');
final Map<String, dynamic> manifestMap = jsonDecode(manifestContent);
Directory doc_directory = await getApplicationDocumentsDirectory();
for (String key in manifestMap.keys) {
var dbPath = doc_directory.path + '/' + key;
if (FileSystemEntity.typeSync(dbPath) == FileSystemEntityType.notFound) {
ByteData data = await rootBundle.load(key);
List<int> bytes = data.buffer.asUint8List(
data.offsetInBytes, data.lengthInBytes);
File file = File(dbPath);
file.createSync(recursive: true);
await file.writeAsBytes(bytes);
}
}
return doc_directory.path + '/assets';
}dataDir
- это директория, в которую были скопированы папки conf, share and license из дистрибутива Face SDK.Добавить в приложение импорт модуля face_sdk_3divi, а также необходимые дополнительные модули:
import 'package:face_sdk_3divi/face_sdk_3divi.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'dart:io';
import 'dart:convert';
import "dart:typed_data";
Работа с плагином
Работа с плагином начинается с инициализации FacerecService
, который позволит создавать другие
примитивы Face SDK для детекции, отслеживания и сравнения лиц.
Пример инициализации объекта FacerecService
в функции main()
:
void main() async {
runApp(MyApp());
FacerecService facerecService = FaceSdkPlugin.createFacerecService(
dataDir + "/conf/facerec",
dataDir + "/license");
Базовые примитивы
Config
Работа с примитивами Face SDK построена на конфигурационных файлах. Например, конфигурационный файл детектора - common_capturere_uld_fda.xml, трекера лиц - video_worker_fdatracker_uld_fda.xml.
Класс Config
инициализируется названием конфигурационного файла и позволяет переопределять его
параметры (например, минимальный Score детектируемых лиц).
RawSample
Данный примитив содержит информацию о задетектированном лице.
Детекция лиц на изображениях
Для детектирования лиц на фотографиях в Face SDK используется компонент Capturer
. Чтобы его создать,
вызовите у инициализированного объекта FacerecService
метод FacerecService.createCapturer
и в качестве аргумента
передайте объект Config
:
Capturer capturer = facerecService.createCapturer(Config("common_capturer4_fda_singleface.xml"));
Для получения детекций используется метод Capturer.capture
, который принимает закодированное изображение:
Uint8List img_bytes = File(filePath).readAsBytesSync(); // reading a file from storage
List<RawSample> detections = capturer.capture(img_bytes); // get detections
В результате выполнения метода вернется список объектов RawSample
, каждый из которых описывает отдельное задетектированное
лицо. Если детекций на изображении не будет, то метод вернет пустой список.
Для получения детекций с камеры устройства можно использовать метод CameraController.takePicture, который сохраняет изображение в памяти устройства, при этом изображение необходимо предварительно загрузить (далее можно удалить сохраненное изображение):
XFile file = await cameraController.takePicture(); // take photo
Uint8List img_bytes = File(file.path).readAsBytesSync(); // load photo
List<RawSample> detections = _capturer.capture(img_bytes); // get detections
File(file.path).delete(); // delete file
Более подробную информацию о CameraController можно найти по ссылке.
Использование CameraController во Flutter подробно описано по ссылке.
Для обрезки лица (кроп) можно использовать cutFaceFromImageBytes
:
final rect = detections[0].getRectangle();
Image _cropImg = await cutFaceFromImageBytes(img_bytes, rect);
Пример виджета, использующего объект Capturer
для детекции лиц через камеру устройства, можно найти в examples/flutter/demo/lib/photo.dart.
Отслеживание лиц на видеопоследовательности и проверка Active Liveness
Для отслеживания лиц и выполнения проверки Active Liveness на видеопоследовательности используется объект VideoWorker
.
Порядок действий при использовании объекта VideoWorker
:
- создать объект
VideoWorker
- получить кадры с камеры (например, через
cameraController.startImageStream
), затем передать их напрямую вVideoWorker
через методVideoWorker.addVideoFrame
или сохранять кадры в переменную и вызыватьVideoWorker.addVideoFrame
(например, обернув в зацикленную функцию StreamBuilder) - получить результаты обработки от
VideoWorker
, вызвав функциюVideoWorker.poolTrackResults
1. Создание объекта VideoWorker
Объект VideoWorker
используется для отслеживания лиц на видеопоследовательности и выполнения проверки Active Liveness.
Для создания объекта VideoWorker
необходимо вызвать метод FacerecService.createVideoWorker
который принимает структуру VideoWorkerParams
, содержащую параметры инициализации:
List<ActiveLivenessCheckType> checks = [
ActiveLivenessCheckType.TURN_LEFT,
ActiveLivenessCheckType.SMILE,
ActiveLivenessCheckType.TURN_DOWN,
];
VideoWorker videoWorker = facerecService.createVideoWorker(
VideoWorkerParams()
.video_worker_config(
Config("video_worker_fdatracker_blf_fda_front.xml")
.overrideParameter("base_angle", 0)
.overrideParameter("enable_active_liveness", 1)
.overrideParameter("active_liveness.apply_horizontal_flip", 0))
.active_liveness_checks_order(checks)
.streams_count(1));
Набор проверок Active Liveness определяется при инициализации свойства active_liveness_checks_order
, которому
передается список действий - сценарий проверки (пример приведен выше).
Доступные проверки Active Liveness:
- TURN_LEFT
- SMILE
- TURN_DOWN
- TURN_RIGHT
- TURN_UP
- BLINK
При использовании видеопоследовательности с камеры необходимо также учитывать базовый угол поворота изображения камеры (можно ориентироваться
на CameraController.description.sensorOrientation).
Для VideoWorker
изображение поворачивать не обязательно, однако необходимо определить параметр base_angle
в соответствии с поворотом камеры.
Для sensorOrientation == 90
установите значение параметра baseAngle = 1,
для sensorOrientation == 270
= 2.
Изображение с фронтальной камеры устройств iOS зеркально отражено по горизонтали - в этом случае необходимо
установить значение "1" для параметра active_liveness.apply_horizontal_flip
.
Пример кода для выбора базового угла
if (controller.description.sensorOrientation == 90)
baseAngle = 1;
else if (controller.description.sensorOrientation == 270)
baseAngle = 2;
double apply_horizontal_flip = 0;
if (Platform.isIOS){
if (controller.description.lensDirection == CameraLensDirection.front)
apply_horizontal_flip = 1;
baseAngle = 0;
}
2. Обработка видео кадров в VideoWorker
Для обработки видеопоследовательности, необходимо передать кадры в VideoWorker
с помощью метода VideoWorker.addVideoFrame
. VideoWorker
принимает кадры в виде массива пикселей RawImageF
.
Поддерживаемые цветовые модели: RGB, BGR, YUV.
Получать кадры с камеры можно через коллбэк ImageStream:
Пример кода с вызовом метода addVideoFrame
NativeDataStruct _data = new NativeDataStruct();
RawImageF convertCameraImg(CameraImage img){
Format format = Format.FORMAT_RGB;
if (img.format.group == ImageFormatGroup.yuv420) {
format = Format.FORMAT_YUV_NV21;
convertRAW(img.planes, _data);
}
else if (img.format.group == ImageFormatGroup.bgra8888) {
format = Format.FORMAT_BGR;
convertBGRA8888(img.planes, _data);
}
else {
print("Unsupported image format");
convertRAW(img.planes, _data);
}
final rawImg = RawImageF(img.width, img.height, format, data.pointer!.cast());
return rawImg;
}
cameraController.startImageStream((CameraImage img) async{
if(!mounted)
return;
int time = new DateTime.now().millisecondsSinceEpoch;
final rawImg = convertCameraImg;
videoWorker.addVideoFrame(rawImg, time);
});
Конвертировать изображения для передачи в `VideoWorker` можно с помощью интегрированных функций `convertRAW`, `convertBGRA8888`.
Для независимой работы ImageStream
и VideoWorker
(вызов addVideoFrame
не должен блокировать video stream)
можно использовать StreamBuilder для асинхронного вызова функции addVideoFrame
.
Пример кода для вызова addVideoFrame c StreamBuilder
Коллбэк потока изображений (сохранение изображения и отметки времени в глобальные переменные):
int _lastImgTimestamp = 0;
CameraImage? _lastImg;
cameraController.startImageStream((CameraImage img) async{
if(!mounted)
return;
int startTime = new DateTime.now().millisecondsSinceEpoch;
setState(() {
_lastImgTimestamp = startTime;
_lastImg = img;
});
});
Асинхронная функция передачи кадров в VideoWorker:
Stream<List<dynamic>> addVF(int prev_time) async* {
final time = _lastImgTimestamp;
var img = _lastImg;
if (!mounted || img == null){
await Future.delayed(const Duration(milliseconds: 50));
yield* addVF(time);
}
final rawImg = convertCameraImg(img!);
videoWorker.addVideoFrame(rawImg, time);
await Future.delayed(const Duration(milliseconds: 50));
yield* addVF(time);
}
Виджет (можно совмещать с другими):
StreamBuilder(
stream: addVF(0),
builder: (context, snapshot){return Text("");},
),
3. Получение результатов отслеживания
Для получения результатов операций VideoWorker
, необходимо вызвать метод VideoWorker.poolTrackResults
, который вернет
структуру с данными о текущих отслеживаемых персонах.
final callbackData = videoWorker.poolTrackResults();
List<RawSample> rawSamples = callbackData.tracking_callback_data.samples;
Статус Active Liveness содержится в TrackingData.tracking_callback_data
:
List<ActiveLivenessStatus> activeLiveness = callbackData.tracking_callback_data.samples_active_liveness_status;
Пример реализации проверок Active Liveness
Определение статуса Active Liveness:
bool livenessFailed = False;
bool livenessPassed = False;
String activeLivenessStatusParse(ActiveLivenessStatus status, Angles angles){
Straing alAction = '';
if (status.verdict == ActiveLiveness.WAITING_FACE_ALIGN) {
alAction = 'Please, look at the camera';
if (angles.yaw > 10)
alAction += ' (turn face →)';
else if (angles.yaw < -10)
alAction += ' (turn face ←)';
else if (angles.pitch > 10)
alAction += ' (turn face ↓)';
else if (angles.pitch < -10)
alAction += ' (turn face ↑)';
}
else if (status.verdict == ActiveLiveness.CHECK_FAIL) {
alAction = 'Active liveness check FAILED';
livenessFailed = true;
_videoWorker.resetTrackerOnStream();
}
else if (status.verdict == ActiveLiveness.ALL_CHECKS_PASSED) {
alAction = 'Active liveness check PASSED';
livenessPassed = true;
_videoWorker.resetTrackerOnStream(); // To get the best shot of face
}
else if (status.verdict == ActiveLiveness.IN_PROGRESS) {
if (status.check_type == ActiveLivenessCheckType.BLINK)
alAction = 'Blink';
else if (status.check_type == ActiveLivenessCheckType.SMILE)
alAction = 'Smile';
else if (status.check_type == ActiveLivenessCheckType.TURN_DOWN)
alAction = 'Turn face down';
else if (status.check_type == ActiveLivenessCheckType.TURN_LEFT) {
if (Platform.isIOS)
alAction = 'Turn face right';
else
alAction = 'Turn face left';
} else if (status.check_type == ActiveLivenessCheckType.TURN_RIGHT) {
if (Platform.isIOS)
alAction = 'Turn face left';
else
alAction = 'Turn face right';
} else if (status.check_type == ActiveLivenessCheckType.TURN_UP)
alAction = 'Turn face up';
}
else if (status.verdict == ActiveLiveness.NOT_COMPUTED)
alAction = 'Active liveness disabled';
return alAction;
}
Получение результатов трекинга:
Straing activeLivenessAction = '';
int progress = 0;
Stream<String> pool() async* {
if (!mounted){
await Future.delayed(const Duration(milliseconds: 50));
yield* pool();
}
final callbackData = _videoWorker.poolTrackResults();
final rawSamples = callbackData.tracking_callback_data.samples;
int progress = livenessProgress;
if (!livenessFailed && !livenessPassed) {
if (callbackData.tracking_callback_data.samples.length == 1) {
ActiveLivenessStatus status = callbackData.tracking_callback_data.samples_active_liveness_status[0];
Angles angles = rawSamples[0].getAngles();
activeLivenessAction = activeLivenessStatusParse(status, angles);
progress = (status.progress_level * 100).toInt();
}
else if (callbackData.tracking_callback_data.samples.length > 1) {
progress = 0;
activeLivenessAction = "Leave one face in the frame ";
}
else {
progress = 0;
activeLivenessAction = "";
}
}
rawSamples.forEach((element) => element.dispose());
setState(() {
livenessProgress = progress;
});
await Future.delayed(const Duration(milliseconds: 50));
yield* pool();
}
Виджет (можно совмещать с другими):
StreamBuilder(
stream: pool(),
builder: (context, snapshot){
return Transform.translate(
offset: Offset(0, 100),
child: Text(activeLivenessAction,
style: new TextStyle(fontSize: 20, backgroundColor: Colors.black))
);
}
),
4. Получение лучшего снимка (best shot) по завершении проверки Active Liveness
Для получения лучшего снимка лица (best shot) необходимо вызвать
метод VideoWorker.resetTrackerOnStream
после успешного прохождения проверок Active Liveness. Метод сбрасывает
состояние трекера и активирует LostTrackingData
в VideoWorker
. Коллбэк LostTrackingData
возвращает лучший снимок лица,
который может быть использован для создания шаблона лица - Template
.
final callbackData = videoWorker.poolTrackResults();
if (callbackData.tracking_lost_callback_data.best_quality_sample != null){
final best_shot = callbackData.tracking_lost_callback_data.best_quality_sample;
final face_template_vw = recognizer.processing(best_shot);
}
Далее face_template_vw
может использоваться для сравнения с другими шаблонами и получения оценки сходства.
Пример кода для получения шаблона лица
После вызова метода videoWorker.poolTrackResults
(пример приведен выше),
в LostTrackingData
будет доступен атрибут best_quality_sample
, который можно использовать для получения шаблона лица.
Template? face_template_vw;
Stream<String> pool() async* {
// .....
// pooling results (see example above)
final best_quality_sample = callbackData.tracking_lost_callback_data.best_quality_sample;
if (face_template_vw == null && livenessPassed && best_quality_sample != null){
face_template_vw = recognizer.processing(best_quality_sample!);
}
setState(() {
if (livenessFailed ){
// liveness fail
}
else (livenessPassed && templ != null){
// livenss passed, return face_template_vw for face comparing
}
});
}
Чтобы получить фото лица, необходимо сохранить лучший снимок CameraImage и обновлять его при получении более высокого качества:
double best_quality = -1e-10;
CamerImage bestImage;
Rectangle bestRect;
Stream<String> pool() async* {
// ... pool and process tracking results ...
if (callbackData.tracking_callback_data.samples.length == 1) {
final sample = callbackData.tracking_callback_data.samples[0];
if (best_quality < callbackData.tracking_callback_data.samples_quality[0]) {
best_quality = callbackData.tracking_callback_data.samples_quality[0];
bestImage = _lastImg;
bestRect = sample.getRectangle();;
}
// ....
}
}
Для обрезки изображения лица используется метод cutFaceFromCameraImage
:
Image cut_face_img = cutFaceFromCameraImage(bestImage, bestRect);
Пример виджета, который использует объект VideoWorker
и выполняет проверку Active Liveness c фронтальной камеры,
можно найти в examples/flutter/demo/lib/photo.dart.
Верификация лиц
Для построения и сравнения шаблонов лиц используется объект Recognizer
. Этот объект создается в результате вызова
метода FacerecService.createRecognizer
, которому необходимо передать аргумент - имя конфигурационного файла распознавателя:
Recognizer recognizer = facerecService.createRecognizer("method10v30_recognizer.xml");
Порядок выполнения операций при сравнении лиц:
- детекция лица
- построение шаблона лица
- сравнение шаблона лица с другими шаблонами
Пример реализации сравнения двух лиц (предполагается, что созданы все необходимые объекты Face SDK и на каждом изображении есть одно лицо):
// Getting the template for the first face
Uint8List imgB1 = File(filePath).readAsBytesSync();
List<RawSample> rawSamples1 = capturer.capture(imgB1);
Template templ1 = recognizer.processing(rawSamples1[0]);
// Getting the template for the second face
Uint8List imgB2 = File(filePath).readAsBytesSync();
List<RawSample> rawSamples2 = capturer.capture(imgB2);
Template templ2 = recognizer.processing(rawSamples2[0]);
// Comparing faces
MatchResult match = recognizer.verifyMatch(templ1, templ2);
Сравнение лица на документе и лица, прошедшего проверку Active Liveness
Для сравнения лица на документе и лица, прошедшего проверку Active Liveness, необходимо построить шаблоны этих лиц.
- Детекция лица на документе и построение шаблона
face_template_idcard
:
XFile file = await cameraController.takePicture(); // take ID-card photo
Uint8List img_bytes = File(file.path).readAsBytesSync(); // load ID-card photo
List<RawSample> detections = capturer.capture(img_bytes); // get detections from photo
File(file.path).delete(); // delete photo
Template face_template_idcard = recognizer.processing(detections[0]); // template building - only one face is expected on the photo!
Получение шаблона лица
face_template_vw
от объектаVideoWorker
после прохождения проверки Active Liveness (пример приведен выше)Сравнение шаблонов
face_template_idcard
иface_template_vw
с помощью методаRecognizer.verifyMatch
:
MatchResult match = recognizer.verifyMatch(face_template_idcard, face_template_vw);
double similarity_score = match.score;