Flutter Plugin
Face SDK предоставляет плагин для Flutter, который позволяет реализовать следующие функции:
- Детекция лиц на фото
- Детекция лиц на видео
- Проверка Active Liveness
- Верификация лиц
Плагин разработан для iOS и Android устройств.
Демонстрационный Flutter сэмпл] с плагином Face SDK доступен в директории examples/flutter/demo дистрибутива Face SDK.
Подключение плагина Face SDK к flutter-проекту
Требования
- Flutter 3.27.0 ≤ версии ≤ 3.27.3
- Dart 3.6.0 ≤ версии ≤ 3.6.1
- Android Studio для Android или XCode для iOS
- Android или 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
dependencies:
flutter:
sdk: flutter
face_sdk_3divi:
path: ../flutter/face_sdk_3divi
Добавить библиотеку libfacerec.so в зависимости от проекта
3.a Для Android-устройств:
- указать путь до директории с библиотекой libfacerec.so
в блоке
sourceSets
файла build.gradle (<project_dir>/android/app/build.gradle)android {
sourceSets {
main {
jniLibs.srcDirs = ["${projectDir}/../../assets/lib"]
}
}
}
- Kotlin
- Java
- указать загрузку native-библиотеки в MainActivity.kt (<project_dir>/android/app/src/main/kotlin/<android_app_name>/MainActivity.kt):
import android.content.pm.ApplicationInfo
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity() {
companion object {
init {
System.loadLibrary("facerec")
}
private const val CHANNEL = "samples.flutter.dev/facesdk"
}
private fun getNativeLibDir(): String {
return applicationInfo.nativeLibraryDir
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
when (call.method) {
"getNativeLibDir" -> result.success(getNativeLibDir())
else -> result.notImplemented()
}
}
}
}указать загрузку native-библиотеки в MainActivity.java (<project_dir>/android/app/src/main/java/<android_app_name>/MainActivity.java):
import android.content.pm.ApplicationInfo;
import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.loader.FlutterLoader;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.FlutterInjector;
public class MainActivity extends FlutterActivity {
static {
System.loadLibrary("facerec");
}
private static final String CHANNEL = "samples.flutter.dev/facesdk";
private String _getNativeLibDir() {
return getApplicationInfo().nativeLibraryDir;
}
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
super.configureFlutterEngine(flutterEngine);
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
.setMethodCallHandler(
(call, result) -> {
if (call.method.equals("getNativeLibDir")) {
String nativeLibDir = _getNativeLibDir();
result.success(nativeLibDir);
} else {
result.notImplemented();
}
}
);
}
}
3.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"
- указать путь до директории с библиотекой libfacerec.so
в блоке
Добавить директории и файлы из дистрибутива 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/faceattributes/
- assets/share/fda/
- assets/share/facerec/recognizers/method12v30/ПримечаниеFlutter не копирует директории рекурсивно, поэтому необходимо указывать каждую директорию с файлами.
Добавить в приложение импорт модуля 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()
:
Future<void> main() async {
runApp(MyApp());
FacerecService facerecService = await FacerecService.createService();
Базовые примитивы
Context
Работа с примитивами Face SDK построена на JSON подобных структурах.
Класс Context
инициализируется unit_type
для создания ProcessingBlock
и позволяет переопределять его
параметры (modification, version, минимальный Score детектируемых лиц).
Также Context
является контейнером для результатов вызова process
у ProcessingBlock
.
ProcessingBlock
Класс ProcessingBlock
используется для процессинга изображений.
Детекция лиц на изображениях
Для детекции лиц на изображениях в Face SDK используется компонент ProcessingBlock
с unit_type
FACE_DETECTOR
. Чтобы его создать,
вызовите у инициализированного объекта FacerecService
метод FacerecService.createContext
и присвойте полю unit_type
значение FACE_DETECTOR
:
ProcessingBlock faceDetector = facerecService.createProcessingBlock({"unit_type": "FACE_DETECTOR"});
Для получения детекций используется метод ProcessingBlock.process
, который принимает закодированное изображение в Context
:
Uint8List imgBytes = File(filePath).readAsBytesSync(); // reading a file from storage
Context data = facerecService.createContextFromEncodedImage(imgBytes); // creating container with image
faceDetector.process(data); // get detections
Context objects = data["objects"]; // detection results
data.dispose(); // delete Context object
Для получения детекций с камеры устройства можно использовать метод CameraController.takePicture, который сохраняет изображение в памяти устройства, при этом изображение необходимо предварительно загрузить (далее можно удалить сохраненное изображение):
XFile file = await cameraController.takePicture(); // take photo
Uint8List imgBytes = File(file.path).readAsBytesSync(); // load photo
Context data = facerecService.createContextFromEncodedImage(imgBytes); // creating container with image
faceDetector.process(data); // get detections
Context objects = data["objects"]; // detection results
File(file.path).delete(); // delete file
data.dispose(); // delete Context object
Более подробную информацию о CameraController можно найти по ссылке.
Использование CameraController во Flutter подробно описано по ссылке.
Для обрезки лица (кроп) можно использовать cutFaceFromImageBytes
:
Context bbox = objects[0]["bbox"];
double x1 = bbox[0].get_value() * imageWidth; // top left x
double y1 = bbox[1].get_value() * imageHeight; // top left y
double x2 = bbox[2].get_value() * imageWidth; // bottom right x
double y2 = bbox[3].get_value() * imageHeight; // bottom right y
Rectangle rect = Rectangle(x1.toInt(), y1.toInt(), (x2 - x1).toInt(), (y2 - y1).toInt());
Image _cropImg = await cutFaceFromImageBytes(imgBytes, rect);
Отслеживание лиц на видеопоследовательности и проверка 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", getBaseAngle(cameraController))
.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
Изображение с фронтальной камеры устройств iOS зеркально отражено по горизонтали - в этом случае необходимо
установить значение "1" для параметра active_liveness.apply_horizontal_flip
.
2. Обработка видео кадров в VideoWorker
Для обработки видеопоследовательности необходимо передать кадры в VideoWorker
с помощью метода VideoWorker.addVideoFrame
. VideoWorker
принимает кадры в виде массива пикселей RawImageF
.
Поддерживаемые цветовые модели: RGB, BGR, YUV.
Получать кадры с камеры можно через коллбэк ImageStream:
Пример кода с вызовом метода addVideoFrame
cameraController.startImageStream((CameraImage img) async{
if(!mounted)
return;
int time = new DateTime.now().millisecondsSinceEpoch;
final rawImg = facerecService.createRawImageFromCameraImage(img, getBaseAngle(cameraController));
videoWorker.addVideoFrame(rawImg, time);
rawImg.dispose();
});
Для независимой работы 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 = facerecService.createRawImageFromCameraImage(img!, getBaseAngle(cameraController))
videoWorker.addVideoFrame(rawImg, time);
await Future.delayed(const Duration(milliseconds: 50));
rawImg.dispose();
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.
Верификация лиц
Для построения шаблонов лиц используется ProcessingBlock
с unit_type FACE_TEMPLATE_EXTRACTOR
. Этот объект создается в результате вызова
метода FacerecService.createProcessingBlock
, которому необходимо передать аргумент - Context
:
ProcessingBlock faceTemplateExtractor = facerecService.createProcessingBlock({"unit_type": "FACE_TEMPLATE_EXTRACTOR", "modification": "30m"});
Порядок выполнения операций при сравнении лиц:
- Детекция лица
- Построение ключевых точек
- Построение шаблона лица
- Сравнение шаблона лица с другими шаблонами
Пример реализации сравнения двух лиц (предполагается, что созданы все необходимые объекты Face SDK и на каждом изображении есть одно лицо):
// Getting the template for the first face
Uint8List imgB1 = File(filePath).readAsBytesSync();
Context data1 = facerecService.createContextFromEncodedImage(imgB1);
faceDetector.process(data1);
faceFitter.process(data1); // unit_type FACE_FITTER
faceTemplateExtractor.process(data1)
ContextTemplate templ1 = data1["objects"][0]["face_template"]["template"].get_value();
// Getting the template for the second face
Uint8List imgB2 = File(filePath).readAsBytesSync();
Context data2 = facerecService.createContextFromEncodedImage(imgB2);
faceDetector.process(data2);
faceFitter.process(data2); // unit_type FACE_FITTER
faceTemplateExtractor.process(data2)
ContextTemplate templ2 = data2["objects"][0]["face_template"]["template"].get_value();
// Comparing faces
ProcessingBlock verificationModule = facerecService.createProcessingBlock({"unit_type": "VERIFICATION_MODULE", "modification": "30m"});
Context verificationData = facerecService.createContext({"template1": templ1, "template2": templ2});
verificationModule.process(verificationData);
Context result = verificationData["result"];
print("Score: ${result["score"].get_value()}");
data1.dispose();
data2.dispose();
verificationData.dispose();
Сравнение лица на документе и лица, прошедшего проверку 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;