Перейти к основному содержимому
Version: 3.15.0

Flutter

Face SDK предоставляет плагин для Flutter, который позволяет реализовать следующие функции:

  • детекция лиц на фото
  • детекция лиц на видео
  • проверка Active Liveness
  • верификация лиц

Плагин разработан для iOS и Android устройств.

Примечание

Демонстрационный Flutter сэмпл с плагином Face SDK доступен в директории examples/flutter/demo дистрибутива Face SDK.

Подключение плагина Face SDK к flutter-проекту#

Требования#

  • Flutter >=2.2 and <=2.8.1
  • Android Studio для Android или XCode для iOS

Подключение плагина#

  1. Для подключения Face SDK к flutter-проекту, компонент "flutter" должен быть установлен с помощью инсталлятора Face SDK или утилиты maintenancetool:

    • Если Face SDK не установлен, следуйте инструкции по установке Приступая к работе. Необходимо выбрать компонент "flutter" в разделе "Выбор компонентов".

    • Если Face SDK установлен без компонента "flutter" (директория flutter отсутствует в корневой директории Face SDK), воспользуйтесь утилитой maintenancetool и установите компонент "flutter", выбрав его в разделе "Выбор компонентов".

  1. Добавить плагины 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"
  1. Добавить библиотеку 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"
  1. Добавить директории и файлы из дистрибутива 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
  1. Указать список директорий и файлов в <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/faceattributes/
    - assets/share/fda/
    - assets/share/facerec/recognizers/method10v30/
    Примечание

    Flutter не копирует директории рекурсивно, поэтому необходимо указывать каждую директорию с файлами.

  1. Добавить в проект функцию копирования ассетов во внутреннюю память приложения (это требуется для корректной работы 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.

  2. Добавить в приложение импорт модуля 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:

  1. создать объект VideoWorker
  2. получить кадры с камеры (например, через cameraController.startImageStream), затем передать их напрямую в VideoWorker через метод VideoWorker.addVideoFrame или сохранять кадры в переменную и вызывать VideoWorker.addVideoFrame (например, обернув в зацикленную функцию StreamBuilder)
  3. получить результаты обработки от 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;
Последнее обновление