Commit 28eeb459 by Chokmongkhon Jansanom

feat: match face.

parent f584375e
......@@ -30,6 +30,9 @@ public class LuxandSdk {
libCode = FSDK.Initialize();
}
availableTemplates = new ArrayList<>();
availableTemplates.add(new FaceTemplate("nax", new byte[]{1, 2}, new byte[]{1, 2}));
return libCode;
}
......@@ -134,7 +137,11 @@ public class LuxandSdk {
if (tracker == null) {
tracker = new FSDK.HTracker();
FSDK.CreateTracker(tracker);
int libCode = FSDK.CreateTracker(tracker);
if (libCode != FSDK.FSDKE_OK) {
Log.e("LUXAND", "Failed to CreateTracker: " + libCode);
}
}
mode = ELuxandMode.MatchTemplate;
......@@ -146,12 +153,14 @@ public class LuxandSdk {
return;
}
int libCode = FSDK.FreeTracker(tracker);
int libCode = FSDK.ClearTracker(tracker);
tracker = null;
mode = ELuxandMode.Detection;
if (libCode != FSDK.FSDKE_OK) {
Log.e("LUXAND", "Failed to FreeTracker: " + libCode);
} else {
Log.e("LUXAND", "=== TRACKER CLEARED ===");
}
}
......@@ -159,11 +168,7 @@ public class LuxandSdk {
availableTemplates = templates;
}
public static List<FaceMatch> TrackFaceForMatchTemplate(byte[] buffer) throws InvalidObjectException {
if (availableTemplates == null) {
throw new InvalidObjectException("No available templates.");
}
public static List<FaceMatch> TrackFaceForMatchTemplate(byte[] buffer, List<FaceTemplate> templates) {
ValidateMatchTemplate();
List<FaceMatch> result = new ArrayList<>();
......@@ -181,63 +186,71 @@ public class LuxandSdk {
if (libCode != FSDK.FSDKE_OK) {
Log.e("LUXAND", "Failed to feed frame: " + libCode);
FSDK.FreeImage(image);
return result;
}
FSDK.TFacePosition pos = new FSDK.TFacePosition();
for (int i = 0; i < ids.length; i++) {
libCode = FSDK.GetTrackerFacePosition(tracker, CAMERA_ID, ids[i], pos);
if (libCode != FSDK.FSDKE_OK) {
Log.e("LUXAND", "Failed to get tracker face position: " + libCode);
if (ids[i] == 0) {
continue;
}
FSDK.FSDK_Features eyes = new FSDK.FSDK_Features();
libCode = FSDK.DetectEyes(image, eyes);
libCode = FSDK.GetTrackerFacePosition(tracker, CAMERA_ID, ids[i], pos);
if (libCode != FSDK.FSDKE_OK) {
Log.e("LUXAND", "Cannot detect eye: " + libCode);
Log.e("LUXAND", "Failed to get tracker face position: " + libCode);
continue;
}
FSDK.FSDK_FaceTemplate eyeTemplate = new FSDK.FSDK_FaceTemplate();
FSDK.GetFaceTemplateUsingEyes(image, eyes, eyeTemplate);
// FSDK.FSDK_Features eyes = new FSDK.FSDK_Features();
// libCode = FSDK.DetectEyes(image, eyes);
//
// if (libCode != FSDK.FSDKE_OK) {
// Log.e("LUXAND", "Cannot detect eye: " + libCode);
// continue;
// }
//
// FSDK.FSDK_FaceTemplate eyeTemplate = new FSDK.FSDK_FaceTemplate();
// FSDK.GetFaceTemplateUsingEyes(image, eyes, eyeTemplate);
// FSDK.FSDK_Features face = new FSDK.FSDK_Features();
// libCode = FSDK.DetectFacialFeaturesInRegion(image, pos, face);
//
// if (libCode != FSDK.FSDKE_OK) {
// Log.e("LUXAND", "Cannot DetectFacialFeaturesInRegion: " + libCode);
// continue;
// }
FSDK.FSDK_Features face = new FSDK.FSDK_Features();
libCode = FSDK.DetectFacialFeatures(image, face);
FSDK.FSDK_FaceTemplate faceTemplate = new FSDK.FSDK_FaceTemplate();
libCode = FSDK.GetFaceTemplateInRegion(image, pos, faceTemplate);
if (libCode != FSDK.FSDKE_OK) {
Log.e("LUXAND", "Cannot detect face: " + libCode);
Log.e("LUXAND", "Cannot GetFaceTemplateUsingFeatures: " + libCode);
continue;
}
FSDK.FSDK_FaceTemplate faceTemplate = new FSDK.FSDK_FaceTemplate();
FSDK.GetFaceTemplateUsingFeatures(image, face, faceTemplate);
FSDK.FreeImage(image);
for (int j = 0; j < availableTemplates.size(); j++) {
for (int j = 0; j < templates.size(); j++) {
FSDK.FSDK_FaceTemplate t = new FSDK.FSDK_FaceTemplate();
t.template = availableTemplates.get(j).EyeTemplate;
float[] similarityEye = new float[1];
libCode = FSDK.MatchFaces(eyeTemplate, t, similarityEye);
if (libCode != FSDK.FSDKE_OK) {
Log.e("LUXAND", "Cannot MatchFaces for eye: " + libCode);
continue;
}
// t.template = availableTemplates.get(j).EyeTemplate;
// float[] similarityEye = new float[1];
// libCode = FSDK.MatchFaces(eyeTemplate, t, similarityEye);
//
// if (libCode != FSDK.FSDKE_OK) {
// Log.e("LUXAND", "Cannot MatchFaces for eye: " + libCode);
// continue;
// }
t.template = availableTemplates.get(j).FaceTemplate;
t.template = templates.get(i).FaceTemplate;
float[] similarityFace = new float[1];
libCode = FSDK.MatchFaces(faceTemplate, t, similarityFace);
if (libCode == FSDK.FSDKE_OK) {
result.add(new FaceMatch(
new FacePosition(pos.xc, pos.yc, pos.w),
availableTemplates.get(i).Id,
similarityEye[0],
templates.get(i).Id,
0,
similarityFace[0]));
break;
} else {
......@@ -246,13 +259,15 @@ public class LuxandSdk {
}
}
FSDK.FreeImage(image);
return result;
}
public static int[] TrackFace(byte[] buffer) {
ValidateTracker();
int[] result = new int[5];
int[] result = new int[3];
HImage image = new HImage();
int libCode = FSDK.LoadImageFromJpegBuffer(image, buffer, buffer.length);
......@@ -352,7 +367,6 @@ public class LuxandSdk {
byte[][] templates = new byte[2][];
HImage image = new HImage();
int libCode = FSDK.LoadImageFromJpegBuffer(image, buffer, buffer.length);
FSDK.FreeImage(image);
if (libCode != FSDK.FSDKE_OK) {
Log.e("LUXAND", "Failed to load image: " + libCode);
......@@ -383,6 +397,8 @@ public class LuxandSdk {
FSDK.GetFaceTemplateUsingFeatures(image, face, ft);
templates[1] = ft.template;
FSDK.FreeImage(image);
return templates;
}
}
......
......@@ -3,6 +3,7 @@ package myhr.facescan.myhr_facescan
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.util.Log
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
......@@ -10,9 +11,13 @@ import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import java.io.ByteArrayOutputStream
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import myhr.facescan.myhr_facescan.models.FaceTemplate
/** MyhrFacescanPlugin */
......@@ -22,6 +27,9 @@ class MyhrFacescanPlugin : FlutterPlugin, MethodCallHandler {
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private lateinit var channel: MethodChannel
private var matchJob: Job? = null
private var detectJob: Job? = null
private var attributeJob: Job? = null
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "myhr_facescan")
......@@ -32,26 +40,19 @@ class MyhrFacescanPlugin : FlutterPlugin, MethodCallHandler {
if (call.method == "activateLuxand") {
result.success(activateLuxand(call.arguments))
} else if (call.method == "detectFace") {
GlobalScope.launch {
val found = detectFace(call.arguments)
result.success(found)
}
detectFace(call.arguments, result)
} else if (call.method == "getFaceTemplate") {
val templates = getFaceTemplate(call.arguments)
var s1 = templates.get(0)?.decodeToString()
var s2 = templates.get(1)?.decodeToString()
var stringArrays = arrayListOf(s1, s2)
result.success(stringArrays)
val data: Map<String, ByteArray> = mapOf(
"eye" to templates[0],
"face" to templates[1]
)
result.success(data)
} else if (call.method == "getFaceAttribute") {
GlobalScope.launch {
val data = getFaceAttribute(call.arguments)
result.success(data)
}
getFaceAttribute(call.arguments, result)
} else if (call.method == "detectFaceForMatch") {
GlobalScope.launch {
val data = detectFaceForMatch(call.arguments)
result.success(data)
}
detectFaceForMatch(call.arguments, result)
} else if (call.method == "setAvailableTemplate") {
val key = call.arguments as Map<String, String>
val ids = key["ids"] as List<String>
......@@ -61,8 +62,10 @@ class MyhrFacescanPlugin : FlutterPlugin, MethodCallHandler {
ids.mapIndexed { index, s -> FaceTemplate(s, eyes.get(index), faces.get(index)) }
LuxandSdk.SetAvailableTempalte(data)
result.success(true)
} else if (call.method == "freeResource") {
LuxandSdk.FreeTracker()
result.success(true)
} else {
result.notImplemented()
}
......@@ -91,24 +94,58 @@ class MyhrFacescanPlugin : FlutterPlugin, MethodCallHandler {
return imageBytes
}
private fun detectFace(data: Any): IntArray {
val imageBytes = getImageBytes(data)
val rotateBitmap = rotateImage(byteArrayToBitmap(imageBytes), 270f)
val bytes = bitmapToByteArray(rotateBitmap, Bitmap.CompressFormat.JPEG, 100)
private fun detectFace(data: Any, result: Result) {
if (detectJob?.isActive == false || detectJob == null) {
detectJob = CoroutineScope(Dispatchers.Main).launch {
val bytes = withContext(Dispatchers.Default) {
val imageBytes = getImageBytes(data)
val rotateBitmap = rotateImage(byteArrayToBitmap(imageBytes), 270f)
bitmapToByteArray(rotateBitmap, Bitmap.CompressFormat.JPEG, 100)
}
return LuxandSdk.TrackFace(bytes)
}
withContext(Dispatchers.Default) {
val faces = LuxandSdk.TrackFace(bytes)
private fun detectFaceForMatch(data: Any): String {
val imageBytes = getImageBytes(data)
val rotateBitmap = rotateImage(byteArrayToBitmap(imageBytes), 270f)
val bytes = bitmapToByteArray(rotateBitmap, Bitmap.CompressFormat.JPEG, 100)
val result = LuxandSdk.TrackFaceForMatchTemplate(bytes)
result.success(faces)
}
}
}
}
return result.map { d ->
d.FaceId + "," + d.SimilarityEye + "," + d.SimilarityFace + "," +
d.Position.CenterX + "," + d.Position.CenterY + "," + d.Position.Width
}.joinToString { ";" }
private fun detectFaceForMatch(data: Any, result: Result) {
if (matchJob?.isActive == false || matchJob == null) {
matchJob = CoroutineScope(Dispatchers.Main).launch {
val bytes = withContext(Dispatchers.Default) {
val imageBytes = getImageBytes(data)
val rotateBitmap = rotateImage(byteArrayToBitmap(imageBytes), 270f)
bitmapToByteArray(rotateBitmap, Bitmap.CompressFormat.JPEG, 100)
}
val key = data as Map<String, String>
val ids = key["ids"] as List<String>
val eyes = key["eyes"] as List<ByteArray>
val faces = key["faces"] as List<ByteArray>
val models = mutableListOf<FaceTemplate>()
for (index in ids.indices) {
models.add(FaceTemplate(ids[index], eyes.get(index), faces.get(index)))
}
withContext(Dispatchers.Default) {
val matches = LuxandSdk.TrackFaceForMatchTemplate(bytes, models)
val matchesString = mutableListOf<String>()
for (m in matches) {
matchesString.add(
m.FaceId + "(,)" + m.SimilarityEye + "(,)" + m.SimilarityFace + "(,)" +
m.Position.CenterX + "(,)" + m.Position.CenterY + "(,)" + m.Position.Width
)
}
result.success(matchesString)
}
}
}
}
private fun getFaceTemplate(data: Any): Array<ByteArray> {
......@@ -120,13 +157,22 @@ class MyhrFacescanPlugin : FlutterPlugin, MethodCallHandler {
return templates
}
private fun getFaceAttribute(data: Any): DoubleArray {
val imageBytes = getImageBytes(data)
val rotateBitmap = rotateImage(byteArrayToBitmap(imageBytes), 270f)
val result = bitmapToByteArray(rotateBitmap, Bitmap.CompressFormat.JPEG, 100)
val key = data as Map<String, String>
val isExpression = key["isExpression"] as Boolean;
return LuxandSdk.GetFaceAttribute(result, isExpression)
private fun getFaceAttribute(data: Any, result: Result) {
if (attributeJob?.isActive == false || attributeJob == null) {
attributeJob = CoroutineScope(Dispatchers.Main).launch {
val bytes = withContext(Dispatchers.Default) {
val imageBytes = getImageBytes(data)
val rotateBitmap = rotateImage(byteArrayToBitmap(imageBytes), 270f)
bitmapToByteArray(rotateBitmap, Bitmap.CompressFormat.JPEG, 100)
}
val key = data as Map<String, String>
val isExpression = key["isExpression"] as Boolean;
val attr = LuxandSdk.GetFaceAttribute(bytes, isExpression)
result.success(attr)
}
}
}
fun bitmapToByteArray(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int): ByteArray? {
......@@ -150,6 +196,12 @@ class MyhrFacescanPlugin : FlutterPlugin, MethodCallHandler {
bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true
)
// val aspectRatio = rotatedBitmap.width.toDouble() / rotatedBitmap.height
// val width = 480
// val height = Math.round(width / aspectRatio).toInt()
//
// val final = Bitmap.createScaledBitmap(rotatedBitmap, width, height, false)
return rotatedBitmap
}
......
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:myhr_facescan/enroll/template_model.dart';
import 'package:myhr_facescan/luxand_controller.dart';
import 'package:myhr_facescan/models/face_template.dart';
import 'package:myhr_facescan/models/template.dart';
import 'package:myhr_facescan_example/main.dart';
import 'package:myhr_facescan_example/template_controller.dart';
class FinishPage extends StatelessWidget {
const FinishPage({super.key, required this.templates});
......@@ -54,15 +52,10 @@ class Controller extends GetxController {
final List<TemplateModel> templates;
save() {
var box = GetStorage();
List<FaceTemplate> faces = box.read('FACES') ?? [];
faces.add(FaceTemplate(
id: tConName.text,
eye: utf8.encode(templates[0].eye),
face: utf8.encode(templates[0].face)));
var con = Get.find<TemplateController>();
box.write('FACES', faces);
con.add(FaceTemplate(
id: tConName.text, eye: templates[0].eye, face: templates[0].face));
Get.snackbar('Success', 'Face & Eye tempalte saved in device.');
Get.offAll(const HomePage());
......
......@@ -17,6 +17,14 @@ class EnrollPage extends StatelessWidget {
child: EnrollFace(
licenseKey: Env.luxandLicenseKey,
onFinish: (templates) {
// var con = Get.find<TemplateController>();
// con.add(FaceTemplate(
// id: 'NaeNaee', eye: templates[0].eye, face: templates[0].face));
// Get.snackbar('Success', 'Face & Eye tempalte saved in device.');
// Get.offAll(const HomePage());
Get.off(LivenessPage(templates: templates));
},
)),
......
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:myhr_facescan/enroll/template_model.dart';
import 'package:myhr_facescan/models/template.dart';
import 'package:myhr_facescan_example/enroll/finish.dart';
import 'package:myhr_facescan_example/env.dart';
import 'package:myhr_facescan/active_liveness/index.dart';
......
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:myhr_facescan_example/active_liveness/index.dart';
import 'package:myhr_facescan_example/enroll/index.dart';
import 'package:myhr_facescan_example/match_face/index.dart';
import 'package:myhr_facescan_example/template_controller.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await GetStorage.init();
runApp(const MyApp());
}
......@@ -26,6 +25,8 @@ class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var con = Get.put(TemplateController());
return Scaffold(
appBar: AppBar(
title: const Text('Luxand x Flutter'),
......@@ -44,11 +45,15 @@ class HomePage extends StatelessWidget {
child: const Text('Match Face')),
ElevatedButton(
onPressed: () async {
var box = GetStorage();
await box.remove('FACES');
con.removeAll();
Get.snackbar('Success', 'Template in device is removed.');
},
child: const Text('Clear storage')),
ElevatedButton(
onPressed: () async {
Get.snackbar('Total: ${con.templates.length}', '');
},
child: const Text('Get total template')),
],
),
),
......
import 'package:flutter/material.dart';
import 'package:get_storage/get_storage.dart';
import 'package:myhr_facescan/luxand_controller.dart';
import 'package:get/get.dart';
import 'package:myhr_facescan/match_template/index.dart';
import 'package:myhr_facescan_example/env.dart';
import 'package:myhr_facescan_example/template_controller.dart';
class MatchFacePage extends StatelessWidget {
const MatchFacePage({super.key});
@override
Widget build(BuildContext context) {
var box = GetStorage();
var templates = box.read('FACES') as List<FaceTemplate>?;
var con = Get.find<TemplateController>();
return Scaffold(
appBar: AppBar(
......@@ -19,7 +18,7 @@ class MatchFacePage extends StatelessWidget {
body: SafeArea(
child: MatchTemplateScanner(
licenseKey: Env.luxandLicenseKey,
templates: templates ?? [],
templates: con.templates,
onFinish: (success, template) {},
)),
);
......
import 'package:get/get.dart';
import 'package:myhr_facescan/models/face_template.dart';
class TemplateController extends GetxController {
var templates = <FaceTemplate>[];
add(FaceTemplate data) {
templates.add(data);
}
removeAll() {
templates.clear();
}
}
......@@ -257,14 +257,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
file:
dependency: transitive
description:
......@@ -338,14 +330,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.6.6"
get_storage:
dependency: "direct main"
description:
name: get_storage
sha256: "39db1fffe779d0c22b3a744376e86febe4ade43bf65e06eab5af707dc84185a2"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
glob:
dependency: transitive
description:
......@@ -510,54 +494,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.0"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
petitparser:
dependency: transitive
description:
......@@ -787,22 +723,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.3"
win32:
dependency: transitive
description:
name: win32
sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480"
url: "https://pub.dev"
source: hosted
version: "5.3.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
url: "https://pub.dev"
source: hosted
version: "1.0.4"
xml:
dependency: transitive
description:
......
......@@ -18,7 +18,6 @@ dependencies:
sdk: flutter
get:
get_storage:
envied: ^0.5.3
myhr_facescan:
# When depending on this package from a real application you should use:
......
......@@ -6,7 +6,7 @@ import 'package:camera/camera.dart';
import 'package:get/get.dart';
import 'package:myhr_facescan/active_liveness/enum.dart';
import 'package:myhr_facescan/active_liveness/face_gesture_verifier.dart';
import 'package:myhr_facescan/enroll/template_model.dart';
import 'package:myhr_facescan/models/template.dart';
import 'package:myhr_facescan/luxand_controller.dart';
import 'package:myhr_facescan/utils/camera.dart';
import 'package:myhr_facescan/utils/throttler.dart';
......@@ -121,7 +121,7 @@ class ActiveLivenessController extends GetxController {
var isExpression = command == EFaceGesture.smile;
var attr = await _luxand.getFaceAttribute(image, isExpression);
if (attr[0] == 0 && attr[1] == 0) {
if (attr.isEmpty || (attr.isNotEmpty && attr[0] == 0 && attr[1] == 0)) {
await _handleFail();
return;
}
......
......@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:myhr_facescan/active_liveness/controller.dart';
import 'package:myhr_facescan/camera_loading.dart';
import 'package:myhr_facescan/enroll/template_model.dart';
import 'package:myhr_facescan/models/template.dart';
class ActiveLivenessVerifier extends StatelessWidget {
const ActiveLivenessVerifier(
......
......@@ -2,8 +2,9 @@ import 'dart:io';
import 'package:camera/camera.dart';
import 'package:get/get.dart';
import 'package:myhr_facescan/enroll/template_model.dart';
import 'package:myhr_facescan/luxand_controller.dart';
import 'package:myhr_facescan/models/face_position.dart';
import 'package:myhr_facescan/models/template.dart';
import 'package:myhr_facescan/utils/camera.dart';
import 'package:myhr_facescan/utils/image.dart';
import 'package:myhr_facescan/utils/throttler.dart';
......@@ -33,9 +34,7 @@ class EnrollController extends GetxController {
final int _imageStreamMilliSecond;
var isFoundFace = false.obs;
var faceX = 0.0.obs;
var faceY = 0.0.obs;
var faceW = 0.0.obs;
var facePosition = FacePosition(x: 0, y: 0, width: 0).obs;
var isCameraReady = false.obs;
var templates = <TemplateModel>[].obs;
......@@ -95,6 +94,11 @@ class EnrollController extends GetxController {
var template = await _luxand.getFaceTemplate(image);
var jpgImage = await convertYUV420toImageColor(image);
if (template.isEmpty) {
_gettingCurrentImage = false;
return;
}
templates.add(TemplateModel(
face: template[1], eye: template[0], image: jpgImage!));
......@@ -112,14 +116,8 @@ class EnrollController extends GetxController {
return;
}
var isFound = data[0] > 0;
isFoundFace.value = isFound;
if (isFound) {
faceX.value = data[0].toDouble();
faceY.value = data[1].toDouble();
faceW.value = data[2].toDouble();
}
isFoundFace.value = true;
facePosition.value = data;
}
});
}
......
......@@ -4,7 +4,7 @@ import 'package:get/get.dart';
import 'package:myhr_facescan/camera_loading.dart';
import 'package:myhr_facescan/enroll/avatar.dart';
import 'package:myhr_facescan/enroll/controller.dart';
import 'package:myhr_facescan/enroll/template_model.dart';
import 'package:myhr_facescan/models/template.dart';
import 'package:myhr_facescan/rectangle_paint.dart';
class EnrollFace extends StatelessWidget {
......@@ -45,14 +45,12 @@ class EnrollFace extends StatelessWidget {
con.camController!.value.previewSize!.height,
con.camController!.value.previewSize!.width),
painter: RectanglePainter(
xc: con.faceX.value,
yc: con.faceY.value,
w: con.faceW.value,
positions: [con.facePosition.value],
paperWidth: con
.camController!.value.previewSize!.height,
paperHeight:
con.camController!.value.previewSize!.width,
topMessage: topMessage),
topMessage: [topMessage]),
)
: Container(),
Positioned.fill(
......
......@@ -4,6 +4,9 @@ import 'dart:typed_data';
import 'package:camera/camera.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:myhr_facescan/models/face_match.dart';
import 'package:myhr_facescan/models/face_position.dart';
import 'package:myhr_facescan/models/face_template.dart';
class LuxandController extends GetxController {
var platform = const MethodChannel("myhr_facescan");
......@@ -31,10 +34,10 @@ class LuxandController extends GetxController {
}
freeResource() async {
await platform.invokeMethod("freeResource");
await platform.invokeMethod<bool>("freeResource");
}
Future<List<int>?> detectFace(CameraImage image) async {
Future<FacePosition?> detectFace(CameraImage image) async {
List<int> strides = Int32List(image.planes.length * 2);
int index = 0;
final bytes = image.planes.map((plane) {
......@@ -52,10 +55,14 @@ class LuxandController extends GetxController {
'strides': strides
});
return result;
if (result == null || result[0] <= 0) {
return null;
}
return FacePosition(x: result[0], y: result[1], width: result[2]);
}
Future<List<String>> getFaceTemplate(CameraImage image) async {
Future<List<Uint8List>> getFaceTemplate(CameraImage image) async {
List<int> strides = Int32List(image.planes.length * 2);
int index = 0;
final bytes = image.planes.map((plane) {
......@@ -66,16 +73,19 @@ class LuxandController extends GetxController {
return plane.bytes;
}).toList();
var data = await platform.invokeMethod<List<Object?>>("getFaceTemplate", {
var data =
await platform.invokeMethod<Map<Object?, Object?>>("getFaceTemplate", {
'platforms': bytes,
'height': image.height,
'width': image.width,
'strides': strides
});
var result = [data![0].toString(), data[1].toString()];
if (data!['face'] == null) {
return [];
}
return result;
return [data['eye'] as Uint8List, data['face'] as Uint8List];
}
Future<List<double>> getFaceAttribute(
......@@ -98,18 +108,27 @@ class LuxandController extends GetxController {
'isExpression': isExpression
});
return data!;
if (data == null) {
return [];
}
return data;
}
Future setAvailableTemplate(List<FaceTemplate> templates) async {
await platform.invokeMethod("setAvailableTemplate", {
'ids': templates.map((e) => e.id).toList(),
'eyes': templates.map((e) => e.eye).toList(),
'faces': templates.map((e) => e.face).toList(),
var ids = templates.map((e) => e.id).toList();
var eyes = templates.map((e) => e.eye).toList();
var faces = templates.map((e) => e.face).toList();
await platform.invokeMethod<bool>("setAvailableTemplate", {
'ids': ids,
'eyes': eyes,
'faces': faces,
});
}
Future<String> detectFaceForMatch(CameraImage image) async {
Future<List<FaceMatch>> detectFaceForMatch(
CameraImage image, List<FaceTemplate> templates) async {
List<int> strides = Int32List(image.planes.length * 2);
int index = 0;
final bytes = image.planes.map((plane) {
......@@ -120,21 +139,39 @@ class LuxandController extends GetxController {
return plane.bytes;
}).toList();
var data = await platform.invokeMethod<String>("detectFaceForMatch", {
var ids = templates.map((e) => e.id).toList();
var eyes = templates.map((e) => e.eye).toList();
var faces = templates.map((e) => e.face).toList();
var data =
await platform.invokeMethod<List<Object?>>("detectFaceForMatch", {
'platforms': bytes,
'height': image.height,
'width': image.width,
'strides': strides,
'ids': ids,
'eyes': eyes,
'faces': faces,
});
return data!;
}
}
var result = <FaceMatch>[];
class FaceTemplate {
final String id;
final Uint8List eye;
final Uint8List face;
if (data == null) {
return result;
}
FaceTemplate({required this.id, required this.eye, required this.face});
for (var d in data) {
var items = d.toString().split('(,)');
result.add(FaceMatch(
id: items[0],
similarityEye: double.parse(items[1]),
similarityFace: double.parse(items[2]),
position: FacePosition(
x: int.parse(items[3]),
y: int.parse(items[4]),
width: int.parse(items[5]))));
}
return result;
}
}
......@@ -2,8 +2,10 @@ import 'dart:io';
import 'package:camera/camera.dart';
import 'package:get/get.dart';
import 'package:myhr_facescan/enroll/template_model.dart';
import 'package:myhr_facescan/luxand_controller.dart';
import 'package:myhr_facescan/models/face_match.dart';
import 'package:myhr_facescan/models/face_template.dart';
import 'package:myhr_facescan/models/template.dart';
import 'package:myhr_facescan/utils/camera.dart';
import 'package:myhr_facescan/utils/throttler.dart';
......@@ -33,6 +35,7 @@ class MatchTemplateController extends GetxController {
final List<FaceTemplate> _templates;
var isCameraReady = false.obs;
var faceMatchs = <FaceMatch>[].obs;
late LuxandController _luxand;
......@@ -41,14 +44,13 @@ class MatchTemplateController extends GetxController {
super.onInit();
await _luxand.initialize();
// await _luxand.setAvailableTemplate(_templates);
await initCamera();
camController!.initialize().then((_) async {
camController!.initialize().then((_) {
isCameraReady.value = true;
previewScale = getCameraPreviewScale(camController!.value.aspectRatio);
await _luxand.setAvailableTemplate(_templates);
// Only open and close camera in iOS for low-tier device
if (Platform.isIOS) {
// _timer = Stream.periodic(const Duration(milliseconds: 500), (v) => v)
......@@ -87,7 +89,9 @@ class MatchTemplateController extends GetxController {
_handleCameraAndroid(CameraImage image) async {
_throttler.run(() async {
var data = await _luxand.detectFaceForMatch(image);
var data = await _luxand.detectFaceForMatch(image, _templates);
faceMatchs.value = data;
});
}
}
......@@ -2,9 +2,10 @@ import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:myhr_facescan/camera_loading.dart';
import 'package:myhr_facescan/enroll/template_model.dart';
import 'package:myhr_facescan/luxand_controller.dart';
import 'package:myhr_facescan/match_template/controller.dart';
import 'package:myhr_facescan/models/face_template.dart';
import 'package:myhr_facescan/models/template.dart';
import 'package:myhr_facescan/rectangle_paint.dart';
class MatchTemplateScanner extends StatelessWidget {
const MatchTemplateScanner({
......@@ -33,6 +34,19 @@ class MatchTemplateScanner extends StatelessWidget {
scale: con.previewScale,
child: CameraPreview(
con.camController!,
child: CustomPaint(
size: Size(con.camController!.value.previewSize!.height,
con.camController!.value.previewSize!.width),
painter: RectanglePainter(
positions: con.faceMatchs.map((d) => d.position).toList(),
paperWidth: con.camController!.value.previewSize!.height,
paperHeight: con.camController!.value.previewSize!.width,
topMessage: con.faceMatchs
.map((d) => '${d.id}, similarity=${d.similarityFace}')
.toList(),
rectColor: Colors.white,
rectStrokeWidth: 2),
),
),
)
: const CameraLoading());
......
import 'package:myhr_facescan/models/face_position.dart';
class FaceMatch {
final String id;
final double similarityEye;
final double similarityFace;
final FacePosition position;
FaceMatch(
{required this.id,
required this.similarityEye,
required this.similarityFace,
required this.position});
}
class FacePosition {
final int x;
final int y;
final int width;
FacePosition({required this.x, required this.y, required this.width});
}
import 'dart:typed_data';
class FaceTemplate {
final String id;
final Uint8List? eye;
final Uint8List face;
FaceTemplate({required this.id, required this.eye, required this.face});
}
import 'dart:typed_data';
import 'package:flutter/material.dart';
class TemplateModel {
final String face;
final String eye;
final Uint8List face;
final Uint8List? eye;
final Image image;
TemplateModel({required this.face, required this.eye, required this.image});
......
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:myhr_facescan/models/face_position.dart';
class RectanglePainter extends CustomPainter {
final double xc;
final double yc;
final double w;
final List<FacePosition> positions;
final double paperWidth;
final double paperHeight;
final String topMessage;
final List<String> topMessage;
Color rectColor;
double rectStrokeWidth;
RectanglePainter(
{super.repaint,
required this.xc,
required this.yc,
required this.w,
required this.positions,
required this.paperWidth,
required this.paperHeight,
required this.topMessage,
......@@ -27,40 +24,49 @@ class RectanglePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final xc = lerpDouble(0, size.width, this.xc / paperWidth)!;
final yc = lerpDouble(0, size.height, this.yc / paperHeight)!;
final width = lerpDouble(0, size.width, w / paperWidth)!;
var paint1 = Paint()
..color = rectColor
..style = PaintingStyle.stroke
..strokeWidth = rectStrokeWidth;
canvas.drawRect(
Rect.fromCenter(center: Offset(xc, yc), width: width, height: width),
paint1);
const textStyle = TextStyle(
color: Colors.green,
// fontSize: 30,
);
var textSpan = TextSpan(
text: topMessage,
style: textStyle,
);
final textPainter = TextPainter(
text: textSpan,
textDirection: TextDirection.ltr,
);
textPainter.layout(
minWidth: 0,
maxWidth: size.width,
);
final xCenter = xc - (width / 2);
final yCenter = yc - (width / 2) - 20;
textPainter.paint(canvas, Offset(xCenter, yCenter));
for (var pos in positions) {
final xc = lerpDouble(0, size.width, pos.x / paperWidth)!;
final yc = lerpDouble(0, size.height, pos.y / paperHeight)!;
final width = lerpDouble(0, size.width, pos.width / paperWidth)!;
canvas.drawRect(
Rect.fromCenter(center: Offset(xc, yc), width: width, height: width),
paint1);
var textSpan = TextSpan(
text: topMessage[positions.indexOf(pos)],
style: textStyle,
);
final textPainter = TextPainter(
text: textSpan,
textDirection: TextDirection.ltr,
);
textPainter.layout(
minWidth: 0,
maxWidth: size.width,
);
final xCenter = xc - (width / 2);
final yCenter = yc - (width / 2) - 20;
textPainter.paint(canvas, Offset(xCenter, yCenter));
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
@override
bool? hitTest(Offset position) {
// TODO: implement hitTest
return super.hitTest(position);
}
}
name: myhr_facescan
description: Enroll face using luxand, verify real people by using active liveness detection
version: 0.1.0
version: 1.0.0
homepage: https://mygit.myhr.co.th/NaeNaee/myhr-facescan
environment:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment