facial_recognition_capturer.dart 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. // ignore_for_file: constant_identifier_names
  2. import 'package:camera/camera.dart' as camera;
  3. import 'package:flutter/foundation.dart';
  4. import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart'
  5. as mlkit;
  6. import 'package:vitalapp/rpc.dart';
  7. import 'package:vitalapp/architecture/storage/storage.dart';
  8. import 'package:vnote_device_plugin/events/event_type.dart';
  9. import 'package:fis_common/logger/logger.dart';
  10. abstract class FacialRecognitionExceptionCode {
  11. static const ABORT = -1;
  12. static const VOID = 0;
  13. static const FACE_NOT_FOUNT = 1001;
  14. static const FACE_OVER_COUNT = 1002;
  15. static const PTHOT_TAKE_ERROR = 2001;
  16. static const PTHOT_TYPE_ERROR = 2002;
  17. static const PTHOT_UPLOAD_FAIL = 2003;
  18. static const RESULT_UNCHECKED = 3001;
  19. static const RESULT_NOT_CLEAR = 3002;
  20. }
  21. class FacialRecognitionException implements Exception {
  22. final int code;
  23. final String message;
  24. FacialRecognitionException(this.code, this.message);
  25. @override
  26. String toString() {
  27. return "[FacialRecognitionException] code: $code, message: $message.";
  28. }
  29. }
  30. /// 人脸识别采集器
  31. class FacialRecognitionCapturer {
  32. /// 重试次数限制
  33. ///
  34. /// - 默认`10`次
  35. final int retryLimit;
  36. /// 单次识别图像已缓存事件
  37. final frameCachedEvent = FEventHandler<String>();
  38. /// 识别成功事件
  39. final successEvent = FEventHandler<String>();
  40. /// 识别失败事件
  41. final failEvent = FEventHandler<FacialRecognitionException>();
  42. /// 识别超时事件 - 超过限制次数
  43. final timeoutEvent = FEventHandler<void>();
  44. FacialRecognitionCapturer({this.retryLimit = 10});
  45. int _workedCount = 0;
  46. _Worker? _worker;
  47. Object? _result;
  48. bool _isBusy = false;
  49. bool _isPause = false;
  50. camera.CameraController? _cameraController;
  51. mlkit.FaceDetector? _faceDetector;
  52. /// 识别结果
  53. Object? get result => _result;
  54. /// 初始化
  55. Future<void> init(
  56. camera.CameraController? cameraController,
  57. mlkit.FaceDetector faceDetector,
  58. ) async {
  59. _cameraController = cameraController;
  60. _faceDetector = faceDetector;
  61. logger.i("FacialRecognitionCapturer init.");
  62. }
  63. /// 暂停
  64. void callPause() {
  65. _isPause = true;
  66. logger.i("FacialRecognitionCapturer pause.");
  67. }
  68. /// 继续
  69. void callContinue() {
  70. _isPause = false;
  71. logger.i("FacialRecognitionCapturer continue.");
  72. }
  73. /// 开启工作
  74. Future<void> run() async {
  75. logger.i("FacialRecognitionCapturer start run...");
  76. _isBusy = true;
  77. while (_isBusy) {
  78. if (_workedCount > retryLimit) {
  79. stop();
  80. timeoutEvent.emit(this, null);
  81. break;
  82. }
  83. if (_isPause == false) {
  84. _workedCount++;
  85. await _doOnce();
  86. }
  87. // 每次间隔100ms检测一次
  88. await Future.delayed(const Duration(milliseconds: 100));
  89. }
  90. logger.i("FacialRecognitionCapturer run finish.");
  91. }
  92. /// 停止工作
  93. void stop() {
  94. logger.i("FacialRecognitionCapturer stop...");
  95. _isBusy = false;
  96. _workedCount = retryLimit + 1;
  97. _worker?.stop();
  98. _worker = null;
  99. logger.i("FacialRecognitionCapturer stop done.");
  100. }
  101. Future<void> _doOnce() async {
  102. try {
  103. final $ = WorkVariables();
  104. $.cameraController = _cameraController;
  105. $.faceDetector = _faceDetector;
  106. $.imgCachedCallback = (value) {
  107. // 通知识别帧已缓存本地
  108. frameCachedEvent.emit(this, value);
  109. };
  110. _worker = _Worker($);
  111. await _worker!.init();
  112. await _worker!.run();
  113. _result = $.result;
  114. if ($.result != null) {
  115. // 暂停等待success回调处理后发送继续工作信号
  116. callPause();
  117. // 识别成功
  118. successEvent.emit(this, $.result!);
  119. }
  120. } on FacialRecognitionException catch (e) {
  121. if (e.code > 0) {
  122. failEvent.emit(this, e);
  123. }
  124. } catch (e) {
  125. logger.e("FacialRecognitionCapturer do once error.", e);
  126. }
  127. }
  128. }
  129. class _Worker {
  130. final steps = <Future<void> Function()>[];
  131. late final WorkVariables $;
  132. bool isContinue = false;
  133. _Worker(WorkVariables variables) {
  134. $ = variables;
  135. steps.add(takePhoto);
  136. steps.add(detectFace);
  137. steps.add(checkFaceNum);
  138. steps.add(uploadFile);
  139. }
  140. Future<void> init() async {}
  141. Future<void> run() async {
  142. isContinue = true;
  143. final count = steps.length;
  144. for (var i = 0; i < count; i++) {
  145. await steps[i].call();
  146. }
  147. isContinue = false;
  148. }
  149. void stop() {
  150. isContinue = false;
  151. }
  152. /// 执行一次拍摄
  153. Future<void> takePhoto() async {
  154. try {
  155. final c = $.cameraController;
  156. if (c == null || c.value.isInitialized == false) {
  157. return abortWithException(
  158. FacialRecognitionExceptionCode.PTHOT_TAKE_ERROR,
  159. "未识别到摄像头",
  160. );
  161. }
  162. if (c.value.isTakingPicture) {
  163. return abort();
  164. }
  165. final file = await c.takePicture();
  166. $.cachedFile = file;
  167. $.imgCachedCallback?.call("");
  168. } on camera.CameraException catch (e) {
  169. logger.e("FacialRecognitionCapturer._Worker takePhoto error.", e);
  170. // TODO: 暂时不提示
  171. // abortWithException(
  172. // FacialRecognitionExceptionCode.PTHOT_TAKE_ERROR,
  173. // e.description ?? "拍摄失败",
  174. // );
  175. abort();
  176. }
  177. }
  178. /// 检测一次人脸
  179. Future<void> detectFace() async {
  180. try {
  181. final image = mlkit.InputImage.fromFilePath($.cachedFile!.path);
  182. final faces = await $.faceDetector!.processImage(image);
  183. $.faceNum = faces.length;
  184. } catch (e) {
  185. logger.e("FacialRecognitionCapturer._Worker detectFace error.", e);
  186. abort();
  187. }
  188. }
  189. /// 检查人脸数量
  190. Future<void> checkFaceNum() async {
  191. final num = $.faceNum;
  192. if (num == 0) {
  193. return abortWithException(
  194. FacialRecognitionExceptionCode.FACE_NOT_FOUNT,
  195. "请将面部保持在识别框内",
  196. );
  197. }
  198. if (num > 1) {
  199. return abortWithException(
  200. FacialRecognitionExceptionCode.FACE_OVER_COUNT,
  201. "请保持只有一张面部在识别范围内",
  202. );
  203. }
  204. }
  205. /// 上传图像
  206. Future<void> uploadFile() async {
  207. try {
  208. final file = $.cachedFile!;
  209. final String fileType = file.path.split('.').last;
  210. if (['png', 'jpg'].contains(fileType) == false) {
  211. return abortWithException(
  212. FacialRecognitionExceptionCode.PTHOT_TYPE_ERROR,
  213. "上传的图像类型错误",
  214. );
  215. }
  216. $.imgCachedCallback?.call(file.path);
  217. final url = await rpc.storage.upload(
  218. file,
  219. fileType: fileType,
  220. );
  221. if (url == null || url.isEmpty) {
  222. return abortWithException(
  223. FacialRecognitionExceptionCode.PTHOT_UPLOAD_FAIL,
  224. "上传失败",
  225. );
  226. }
  227. $.result = url;
  228. } catch (e) {
  229. logger.e("FacialRecognitionCapturer._Worker upload file error.", e);
  230. abortWithException(
  231. FacialRecognitionExceptionCode.PTHOT_UPLOAD_FAIL,
  232. "上传人像失败",
  233. );
  234. } finally {
  235. $.imgCachedCallback?.call("");
  236. }
  237. }
  238. void abort() {
  239. isContinue = false;
  240. throw FacialRecognitionException(
  241. FacialRecognitionExceptionCode.ABORT,
  242. "中断",
  243. );
  244. }
  245. void abortWithException(int code, String message) {
  246. isContinue = false;
  247. throw FacialRecognitionException(code, message);
  248. }
  249. }
  250. class WorkVariables {
  251. camera.CameraController? cameraController;
  252. mlkit.FaceDetector? faceDetector;
  253. ValueChanged<String>? imgCachedCallback;
  254. camera.XFile? cachedFile;
  255. int faceNum = 0;
  256. String? result;
  257. }