controller.dart 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'package:camera/camera.dart';
  4. import 'package:fis_jsonrpc/rpc.dart';
  5. import 'package:flutter/foundation.dart';
  6. import 'package:flutter/material.dart';
  7. import 'package:flutter/services.dart';
  8. import 'package:get/get.dart';
  9. import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
  10. import 'package:image_gallery_saver/image_gallery_saver.dart';
  11. import 'package:vitalapp/architecture/storage/storage.dart';
  12. import 'package:vitalapp/architecture/utils/prompt_box.dart';
  13. import 'package:vitalapp/managers/interfaces/cache.dart';
  14. import 'package:fis_common/logger/logger.dart';
  15. import 'package:vitalapp/rpc.dart';
  16. import 'package:vitalapp/store/store.dart';
  17. import 'dart:ui' as ui;
  18. import 'index.dart';
  19. class FacialRecognitionController extends GetxController
  20. with WidgetsBindingObserver {
  21. FacialRecognitionController({required this.mode});
  22. final FacialRecognitionMode mode;
  23. final state = FacialRecognitionState();
  24. List<CameraDescription> _cameras = <CameraDescription>[];
  25. List<CameraDescription> get cameras => _cameras;
  26. CameraController? kCameraController;
  27. double _minAvailableExposureOffset = 0.0;
  28. double _maxAvailableExposureOffset = 0.0;
  29. double _minAvailableZoom = 1.0;
  30. double _maxAvailableZoom = 1.0;
  31. double _currentScale = 1.0;
  32. double _baseScale = 1.0;
  33. // 屏幕上手指数量
  34. int pointers = 0;
  35. // 当前需要返回的身份信息
  36. IdCardInfoModel idCardInfo = IdCardInfoModel();
  37. /// 开始缩放
  38. void handleScaleStart(ScaleStartDetails details) {
  39. _baseScale = _currentScale;
  40. }
  41. /// 当前捕获帧的人脸列表
  42. List<Face> kFrameFacesResult = [];
  43. /// 当前捕获帧大小
  44. Size kFrameImageSize = Size.zero;
  45. /// 缩放更新
  46. Future<void> handleScaleUpdate(ScaleUpdateDetails details) async {
  47. // When there are not exactly two fingers on screen don't scale
  48. if (kCameraController == null || pointers != 2) {
  49. return;
  50. }
  51. // 屏蔽缩放
  52. // _currentScale = (_baseScale * details.scale)
  53. // .clamp(_minAvailableZoom, _maxAvailableZoom);
  54. // await kCameraController!.setZoomLevel(_currentScale);
  55. }
  56. /// 修改对焦点 [暂不执行]
  57. void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) {
  58. if (kCameraController == null) {
  59. return;
  60. }
  61. final CameraController cameraController = kCameraController!;
  62. final Offset offset = Offset(
  63. details.localPosition.dx / constraints.maxWidth,
  64. details.localPosition.dy / constraints.maxHeight,
  65. );
  66. cameraController.setExposurePoint(offset);
  67. cameraController.setFocusPoint(offset);
  68. }
  69. /// 初始化相机
  70. Future<void> initAvailableCameras() async {
  71. try {
  72. _cameras = await availableCameras();
  73. if (_cameras.isNotEmpty) {
  74. // state.isCameraReady = true;
  75. }
  76. // print("cameras: ${_cameras.length}");
  77. } on CameraException catch (e) {
  78. logger.e("cameras: ${e.code} ${e.description}");
  79. }
  80. }
  81. /// 启动指定相机
  82. Future<void> openNewCamera(CameraDescription cameraDescription) async {
  83. final CameraController? oldController = kCameraController;
  84. if (oldController != null) {
  85. // `kCameraController` needs to be set to null before getting disposed,
  86. // to avoid a race condition when we use the kCameraController that is being
  87. // disposed. This happens when camera permission dialog shows up,
  88. // which triggers `didChangeAppLifecycleState`, which disposes and
  89. // re-creates the kCameraController.
  90. kCameraController = null;
  91. await oldController.dispose();
  92. }
  93. final CameraController cameraController = CameraController(
  94. cameraDescription,
  95. ResolutionPreset.max,
  96. enableAudio: false,
  97. imageFormatGroup: ImageFormatGroup.jpeg,
  98. );
  99. kCameraController = cameraController;
  100. // If the kCameraController is updated then update the UI.
  101. cameraController.addListener(() {
  102. if (cameraController.value.hasError) {
  103. PromptBox.toast(
  104. "Camera error ${cameraController.value.errorDescription}");
  105. }
  106. });
  107. try {
  108. await cameraController.initialize();
  109. await Future.wait(<Future<Object?>>[
  110. // The exposure mode is currently not supported on the web.
  111. ...!kIsWeb
  112. ? <Future<Object?>>[
  113. cameraController.getMinExposureOffset().then(
  114. (double value) => _minAvailableExposureOffset = value),
  115. cameraController
  116. .getMaxExposureOffset()
  117. .then((double value) => _maxAvailableExposureOffset = value)
  118. ]
  119. : <Future<Object?>>[],
  120. cameraController
  121. .getMaxZoomLevel()
  122. .then((double value) => _maxAvailableZoom = value),
  123. cameraController
  124. .getMinZoomLevel()
  125. .then((double value) => _minAvailableZoom = value),
  126. ]);
  127. } on CameraException catch (e) {
  128. switch (e.code) {
  129. case 'CameraAccessDenied':
  130. PromptBox.toast('You have denied camera access.');
  131. break;
  132. case 'CameraAccessDeniedWithoutPrompt':
  133. // iOS only
  134. PromptBox.toast('Please go to Settings app to enable camera access.');
  135. break;
  136. case 'CameraAccessRestricted':
  137. // iOS only
  138. PromptBox.toast('Camera access is restricted.');
  139. break;
  140. case 'AudioAccessDenied':
  141. PromptBox.toast('You have denied audio access.');
  142. break;
  143. case 'AudioAccessDeniedWithoutPrompt':
  144. // iOS only
  145. PromptBox.toast('Please go to Settings app to enable audio access.');
  146. break;
  147. case 'AudioAccessRestricted':
  148. // iOS only
  149. PromptBox.toast('Audio access is restricted.');
  150. break;
  151. default:
  152. PromptBox.toast('Error: ${e.code}\n${e.description}');
  153. break;
  154. }
  155. }
  156. }
  157. /// 遍历当前相机列表并启动后置相机
  158. void openBackCamera() async {
  159. if (_cameras.isEmpty) {
  160. PromptBox.toast('Error: No cameras found.');
  161. } else {
  162. for (CameraDescription cameraDescription in _cameras) {
  163. if (cameraDescription.lensDirection == CameraLensDirection.back) {
  164. await openNewCamera(cameraDescription);
  165. lockCaptureOrientation();
  166. state.isUsingFrontCamera = false;
  167. update();
  168. state.isCameraReady = true;
  169. break;
  170. }
  171. }
  172. }
  173. }
  174. /// 遍历当前相机列表并启动前置相机
  175. void openFrontCamera() async {
  176. if (_cameras.isEmpty) {
  177. PromptBox.toast('Error: No cameras found.');
  178. } else {
  179. for (CameraDescription cameraDescription in _cameras) {
  180. if (cameraDescription.lensDirection == CameraLensDirection.front) {
  181. await openNewCamera(cameraDescription);
  182. state.isUsingFrontCamera = true;
  183. lockCaptureOrientation();
  184. update();
  185. state.isCameraReady = true;
  186. break;
  187. }
  188. }
  189. }
  190. }
  191. /// 相机锁定旋转
  192. Future<void> lockCaptureOrientation() async {
  193. final CameraController? cameraController = kCameraController;
  194. if (cameraController == null || !cameraController.value.isInitialized) {
  195. PromptBox.toast('Error: select a camera first.');
  196. return;
  197. }
  198. if (!cameraController.value.isCaptureOrientationLocked) {
  199. try {
  200. await cameraController
  201. .lockCaptureOrientation(DeviceOrientation.landscapeLeft);
  202. } on CameraException catch (e) {
  203. PromptBox.toast('Error: ${e.code}\n${e.description}');
  204. }
  205. } else {
  206. PromptBox.toast('Rotation lock is already enabled.');
  207. }
  208. }
  209. /// 执行一次拍摄
  210. Future<XFile?> takePicture() async {
  211. final CameraController? cameraController = kCameraController;
  212. if (cameraController == null || !cameraController.value.isInitialized) {
  213. PromptBox.toast('Error: select a camera first.');
  214. return null;
  215. }
  216. if (cameraController.value.isTakingPicture) {
  217. // A capture is already pending, do nothing.
  218. return null;
  219. }
  220. try {
  221. final XFile file = await cameraController.takePicture();
  222. return file;
  223. } on CameraException catch (e) {
  224. PromptBox.toast('Error: ${e.code}\n${e.description}');
  225. return null;
  226. }
  227. }
  228. /// 测试图像文件缓存,print 遍历输出
  229. void debugShowCache() async {
  230. final cacheManager = Get.find<ICacheManager>();
  231. double cacheSize = await cacheManager.getCacheSize();
  232. double imageCacheSize = await cacheManager.getImageCacheSize();
  233. debugPrint('cacheSize = $cacheSize : ${formatSize(cacheSize)}');
  234. debugPrint(
  235. 'imageCacheSize = $imageCacheSize : ${formatSize(imageCacheSize)}');
  236. }
  237. /// 文件大小转为可读 Str
  238. static String formatSize(double value) {
  239. List<String> unitArr = ['B', 'K', 'M', 'G'];
  240. int index = 0;
  241. while (value > 1024) {
  242. index++;
  243. value = value / 1024;
  244. }
  245. String size = value.toStringAsFixed(2);
  246. return size + unitArr[index];
  247. }
  248. /// 保存到相册
  249. void saveImageToGallery(XFile image) async {
  250. // 获取图像的字节数据
  251. Uint8List bytes = await image.readAsBytes();
  252. // 将图像保存到相册
  253. await ImageGallerySaver.saveImage(bytes, quality: 100);
  254. }
  255. /// 处理图像裁切
  256. Future<String> clipLocalImage(XFile soureceImage, double scale) async {
  257. assert(scale >= 1, 'scale must be greater than 1');
  258. // 获取图像的字节数据
  259. Uint8List bytes = await soureceImage.readAsBytes();
  260. var codec = await ui.instantiateImageCodec(bytes);
  261. var nextFrame = await codec.getNextFrame();
  262. var image = nextFrame.image;
  263. Rect src = Rect.fromLTWH(
  264. (scale - 1) / 2 / scale * image.width.toDouble(),
  265. (scale - 1) / 2 / scale * image.height.toDouble(),
  266. image.width.toDouble() / scale,
  267. image.height.toDouble() / scale,
  268. );
  269. Rect dst =
  270. Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble());
  271. ui.Image croppedImage = await getCroppedImage(image, src, dst);
  272. ByteData? newImageBytes =
  273. await croppedImage.toByteData(format: ui.ImageByteFormat.png);
  274. if (newImageBytes == null) {
  275. return '';
  276. }
  277. Uint8List newImageUint8List = newImageBytes.buffer.asUint8List();
  278. /// FIXME 不要存到相册而是存到临时目录
  279. Map<Object?, Object?> result =
  280. await ImageGallerySaver.saveImage(newImageUint8List, quality: 100);
  281. String jsonString = jsonEncode(result);
  282. Map<String, dynamic> json = jsonDecode(jsonString);
  283. String? filePath = json['filePath'];
  284. return filePath ?? '';
  285. }
  286. /// 获取图像文件的图像尺寸
  287. Future<Size> getImageSize(XFile soureceImage) async {
  288. // 获取图像的字节数据
  289. Uint8List bytes = await soureceImage.readAsBytes();
  290. var codec = await ui.instantiateImageCodec(bytes);
  291. var nextFrame = await codec.getNextFrame();
  292. var image = nextFrame.image;
  293. return Size(image.width.toDouble(), image.height.toDouble());
  294. }
  295. /// 获取裁切后的图像
  296. Future<ui.Image> getCroppedImage(ui.Image image, Rect src, Rect dst) {
  297. var pictureRecorder = ui.PictureRecorder();
  298. Canvas canvas = Canvas(pictureRecorder);
  299. canvas.drawImageRect(image, src, dst, Paint());
  300. return pictureRecorder.endRecording().toImage(
  301. dst.width.floor(),
  302. dst.height.floor(),
  303. );
  304. }
  305. /// 发生开始人脸识别事件
  306. void onCaptureFaceButtonPressed() {
  307. // runDetectionTimer();
  308. /// 接口测试
  309. ///
  310. if (mode == FacialRecognitionMode.faceRecognition) {
  311. doFacialRecognitionTimes = 0;
  312. state.isRunningFaceRecognition = true;
  313. doFacialRecognition(); // 人脸识别
  314. } else {
  315. rpcTest2(); // 人脸录入
  316. }
  317. }
  318. /// 人脸识别执行次数
  319. int doFacialRecognitionTimes = 0;
  320. void doFacialRecognition() async {
  321. doFacialRecognitionTimes++;
  322. if (doFacialRecognitionTimes == 10) {
  323. // 尝试十次后宣告失败
  324. state.isRunningFaceRecognition = false;
  325. PromptBox.toast('人脸识别失败,请稍后重试');
  326. return;
  327. }
  328. if (kCameraController == null) {
  329. state.isRunningFaceRecognition = false;
  330. PromptBox.toast('相机启动失败,请稍后重试');
  331. return;
  332. }
  333. final XFile? file = await takePicture();
  334. if (file != null) {
  335. faceDetector = FaceDetector(options: FaceDetectorOptions());
  336. // faceDetector =
  337. // FaceDetector(options: FaceDetectorOptions(enableContours: true));
  338. int faceNum =
  339. await doDetection(faceDetector, file.path); // max 分辨率下检测用时大约 100ms
  340. if (faceNum == 0) {
  341. PromptBox.toast('请将面部保持在识别框内');
  342. await Future.delayed(const Duration(seconds: 2));
  343. } else if (faceNum > 1) {
  344. PromptBox.toast('请保持只有一张面部在识别范围内');
  345. await Future.delayed(const Duration(seconds: 2));
  346. } else {
  347. /// TODO 上传图像到云然后传给后端
  348. final url = await rpc.storage.upload(
  349. file,
  350. fileType: 'png',
  351. );
  352. print('⭐⭐⭐⭐⭐⭐⭐⭐ url: $url');
  353. try {
  354. PatientBaseDTO result =
  355. await rpc.patient.getPatientBaseByFaceImageAsync(
  356. GetPatientBaseByFaceImageRequest(
  357. token: Store.user.token,
  358. image: url,
  359. ),
  360. );
  361. print(result);
  362. if (result.faceScanErrorType == FaceScanErrorTypeEnum.Success) {
  363. PromptBox.toast('人脸识别成功,身份证号 ${result.cardNo}}');
  364. finishFaceDetection(result);
  365. } else {
  366. // 暂先将 API 错误和 查无此人都归为识别错误
  367. PromptBox.toast('人脸识别失败: $doFacialRecognitionTimes');
  368. await Future.delayed(const Duration(seconds: 2));
  369. }
  370. } catch (e) {
  371. logger.e("getPatientBaseByFaceImageAsync failed: $e", e);
  372. }
  373. }
  374. }
  375. doFacialRecognition();
  376. }
  377. /// 人脸录入测试
  378. void rpcTest2() async {
  379. if (kCameraController == null) {
  380. return;
  381. }
  382. final XFile? file = await takePicture();
  383. if (file != null) {
  384. faceDetector = FaceDetector(options: FaceDetectorOptions());
  385. // faceDetector =
  386. // FaceDetector(options: FaceDetectorOptions(enableContours: true));
  387. int faceNum =
  388. await doDetection(faceDetector, file.path); // max 分辨率下检测用时大约 100ms
  389. if (faceNum == 0) {
  390. PromptBox.toast('请将面部保持在识别框内');
  391. return;
  392. } else if (faceNum > 1) {
  393. PromptBox.toast('请保持只有一张面部在识别范围内');
  394. return;
  395. }
  396. /// TODO 上传图像到云然后传给后端
  397. final url = await rpc.storage.upload(
  398. file,
  399. fileType: 'png',
  400. );
  401. print('⭐⭐⭐⭐⭐⭐⭐⭐ url: $url');
  402. try {
  403. SavePersonDTO result =
  404. await rpc.patient.savePatientBaseByFaceImageAsync(
  405. SavePatientBaseByFaceImageRequest(
  406. cardNo: '320581199906190615',
  407. token: Store.user.token,
  408. image: url,
  409. ),
  410. );
  411. print(result);
  412. if (result.success) {
  413. PromptBox.toast('人脸数据存入成功');
  414. } else {
  415. PromptBox.toast('人脸数据存入失败: ${result.errMessage}');
  416. }
  417. } catch (e) {
  418. logger.e("savePatientBaseByFaceImageAsync failed: $e", e);
  419. }
  420. // if (timer.tick == 1 || kFrameImageSize == Size.zero) {
  421. // Size imageSize = await getImageSize(file);
  422. // kFrameImageSize = imageSize;
  423. // }
  424. // int kTime = DateTime.now().millisecondsSinceEpoch;
  425. // print('⭐⭐⭐⭐⭐⭐⭐⭐ capture time: ${kTime - lastCaptureTime} ms');
  426. // lastCaptureTime = kTime;
  427. // /// 记录用时 ms
  428. // int endTime = DateTime.now().millisecondsSinceEpoch;
  429. // print('⭐⭐⭐⭐⭐⭐⭐⭐ detection time: ${endTime - lastCaptureTime} ms');
  430. // update(['face_bounding_box']);
  431. // if (timer.tick >= 10) {
  432. // finishFaceDetection(); // TODO 接入真实的判断条件
  433. // }
  434. }
  435. }
  436. /// 发生结束录制视频事件
  437. void onStopButtonPressed() {
  438. stopVideoRecording().then((XFile? file) {
  439. if (file != null) {
  440. PromptBox.toast('Video recorded to ${file.path}');
  441. // videoFile = file;
  442. // _startVideoPlayer();
  443. }
  444. update();
  445. });
  446. }
  447. /// 发生开始录制视频事件
  448. void onVideoRecordButtonPressed() {
  449. startVideoRecording().then((_) {
  450. update();
  451. });
  452. }
  453. /// 暂停录制视频
  454. void onPauseButtonPressed() {
  455. pauseVideoRecording().then((_) {
  456. update();
  457. });
  458. }
  459. /// 恢复视频录制
  460. void onResumeButtonPressed() {
  461. resumeVideoRecording().then((_) {
  462. update();
  463. });
  464. }
  465. /// 开始录制视频
  466. Future<void> startVideoRecording() async {
  467. final CameraController? cameraController = kCameraController;
  468. if (cameraController == null || !cameraController.value.isInitialized) {
  469. PromptBox.toast('Error: select a camera first.');
  470. return;
  471. }
  472. if (cameraController.value.isRecordingVideo) {
  473. // A recording is already started, do nothing.
  474. return;
  475. }
  476. try {
  477. await cameraController.startVideoRecording();
  478. } on CameraException catch (e) {
  479. PromptBox.toast('Error: ${e.code}\n${e.description}');
  480. return;
  481. }
  482. }
  483. /// 停止录制视频
  484. Future<XFile?> stopVideoRecording() async {
  485. final CameraController? cameraController = kCameraController;
  486. if (cameraController == null || !cameraController.value.isRecordingVideo) {
  487. return null;
  488. }
  489. try {
  490. return cameraController.stopVideoRecording();
  491. } on CameraException catch (e) {
  492. PromptBox.toast('Error: ${e.code}\n${e.description}');
  493. return null;
  494. }
  495. }
  496. /// 暂停录制视频
  497. Future<void> pauseVideoRecording() async {
  498. final CameraController? cameraController = kCameraController;
  499. if (cameraController == null || !cameraController.value.isRecordingVideo) {
  500. return;
  501. }
  502. try {
  503. await cameraController.pauseVideoRecording();
  504. } on CameraException catch (e) {
  505. PromptBox.toast('Error: ${e.code}\n${e.description}');
  506. rethrow;
  507. }
  508. }
  509. /// 恢复视频录制
  510. Future<void> resumeVideoRecording() async {
  511. final CameraController? cameraController = kCameraController;
  512. if (cameraController == null || !cameraController.value.isRecordingVideo) {
  513. return;
  514. }
  515. try {
  516. await cameraController.resumeVideoRecording();
  517. } on CameraException catch (e) {
  518. PromptBox.toast('Error: ${e.code}\n${e.description}');
  519. rethrow;
  520. }
  521. }
  522. /// 在 widget 内存中分配后立即调用。
  523. @override
  524. void onInit() async {
  525. // await initCamera();
  526. super.onInit();
  527. WidgetsBinding.instance.addObserver(this);
  528. }
  529. /// 在 onInit() 之后调用 1 帧。这是进入的理想场所
  530. @override
  531. void onReady() async {
  532. super.onReady();
  533. await initAvailableCameras();
  534. openFrontCamera();
  535. }
  536. /// 在 [onDelete] 方法之前调用。
  537. @override
  538. void onClose() {
  539. super.onClose();
  540. final cacheManager = Get.find<ICacheManager>();
  541. cacheManager.clearApplicationImageCache();
  542. closeDetector();
  543. WidgetsBinding.instance.removeObserver(this);
  544. final CameraController? cameraController = kCameraController;
  545. if (cameraController != null) {
  546. kCameraController = null;
  547. cameraController.dispose();
  548. }
  549. }
  550. /// dispose 释放内存
  551. @override
  552. void dispose() {
  553. super.dispose();
  554. }
  555. @override
  556. void didChangeAppLifecycleState(AppLifecycleState state) async {
  557. super.didChangeAppLifecycleState(state);
  558. final CameraController? cameraController = kCameraController;
  559. // App state changed before we got the chance to initialize.
  560. if (cameraController == null || !cameraController.value.isInitialized) {
  561. return;
  562. }
  563. if (state == AppLifecycleState.inactive) {
  564. cameraController.dispose();
  565. this.state.isCameraReady = false;
  566. } else if (state == AppLifecycleState.resumed) {
  567. await openNewCamera(cameraController.description);
  568. this.state.isCameraReady = true;
  569. }
  570. }
  571. /// 完成人脸识别
  572. void finishFaceDetection(PatientBaseDTO patient) {
  573. final result = FaceRecognitionResult(
  574. success: true,
  575. cardNo: idCardInfo.idCardNumber,
  576. name: idCardInfo.idCardName,
  577. nation: idCardInfo.idCardNation,
  578. gender:
  579. idCardInfo.idCardGender == '男' ? GenderEnum.Male : GenderEnum.Female,
  580. birthday: DateTime.now(),
  581. address: idCardInfo.idCardAddress,
  582. );
  583. // Get.back<FaceRecognitionResult>(
  584. // result: result,
  585. // );
  586. // TODO 识别成功后的切换逻辑
  587. }
  588. /// WIP
  589. /// 面部识别 基于 Google's ML Kit
  590. ///
  591. InputImage inputImage = InputImage.fromFilePath('');
  592. FaceDetector faceDetector = FaceDetector(options: FaceDetectorOptions());
  593. // 进行一次人脸检测 (返回人脸数量)
  594. Future<int> doDetection(
  595. FaceDetector faceDetector,
  596. String imagePath,
  597. ) async {
  598. inputImage = InputImage.fromFilePath(imagePath);
  599. // inputImage = image;
  600. final List<Face> faces = await faceDetector.processImage(inputImage);
  601. kFrameFacesResult = [];
  602. kFrameFacesResult.addAll(faces);
  603. // for (Face face in faces) {
  604. // final Rect boundingBox = face.boundingBox;
  605. // final double? rotX =
  606. // face.headEulerAngleX; // Head is tilted up and down rotX degrees
  607. // final double? rotY =
  608. // face.headEulerAngleY; // Head is rotated to the right rotY degrees
  609. // final double? rotZ =
  610. // face.headEulerAngleZ; // Head is tilted sideways rotZ degrees
  611. // // If landmark detection was enabled with FaceDetectorOptions (mouth, ears,
  612. // // eyes, cheeks, and nose available):
  613. // final FaceLandmark? leftEar = face.landmarks[FaceLandmarkType.leftEar];
  614. // if (leftEar != null) {
  615. // final Point<int> leftEarPos = leftEar.position;
  616. // }
  617. // // If classification was enabled with FaceDetectorOptions:
  618. // if (face.smilingProbability != null) {
  619. // final double? smileProb = face.smilingProbability;
  620. // }
  621. // // If face tracking was enabled with FaceDetectorOptions:
  622. // if (face.trackingId != null) {
  623. // final int? id = face.trackingId;
  624. // }
  625. // }
  626. return kFrameFacesResult.length;
  627. }
  628. // bool isDetectionRunning = false;
  629. Timer? _detectionTimer;
  630. /// 开始持续检测人脸
  631. void runDetectionTimer() {
  632. if (_detectionTimer != null) {
  633. _detectionTimer!.cancel();
  634. _detectionTimer = null;
  635. faceDetector.close();
  636. state.isShowFaceRecognitionResult = false;
  637. return;
  638. }
  639. faceDetector =
  640. FaceDetector(options: FaceDetectorOptions(enableContours: true));
  641. state.isShowFaceRecognitionResult = true;
  642. /// 记录最后一次拍摄的时间
  643. int lastCaptureTime = DateTime.now().millisecondsSinceEpoch;
  644. _detectionTimer = Timer.periodic(
  645. const Duration(milliseconds: 300), // max 分辨率下拍摄用时大约 500ms-800ms
  646. (timer) async {
  647. if (kCameraController == null) {
  648. return;
  649. }
  650. final XFile? file = await takePicture();
  651. if (file != null) {
  652. if (timer.tick == 1 || kFrameImageSize == Size.zero) {
  653. Size imageSize = await getImageSize(file);
  654. kFrameImageSize = imageSize;
  655. }
  656. int kTime = DateTime.now().millisecondsSinceEpoch;
  657. print('⭐⭐⭐⭐⭐⭐⭐⭐ capture time: ${kTime - lastCaptureTime} ms');
  658. lastCaptureTime = kTime;
  659. /// 记录用时 ms
  660. await doDetection(faceDetector, file.path); // max 分辨率下检测用时大约 100ms
  661. int endTime = DateTime.now().millisecondsSinceEpoch;
  662. print('⭐⭐⭐⭐⭐⭐⭐⭐ detection time: ${endTime - lastCaptureTime} ms');
  663. update(['face_bounding_box']);
  664. if (timer.tick >= 10) {
  665. // finishFaceDetection(); // TODO 接入真实的判断条件
  666. }
  667. }
  668. },
  669. );
  670. }
  671. /// 用于将读取的视频流传给 Google ML
  672. InputImage cameraImageToInputImage(CameraImage cameraImage) {
  673. return InputImage.fromBytes(
  674. bytes: _concatenatePlanes(cameraImage.planes),
  675. metadata: InputImageMetadata(
  676. size: Size(cameraImage.width.toDouble(), cameraImage.height.toDouble()),
  677. rotation: InputImageRotation.rotation0deg,
  678. format: _getInputImageFormat(cameraImage.format.group),
  679. bytesPerRow: cameraImage.planes[0].bytesPerRow,
  680. ),
  681. );
  682. }
  683. /// 辅助函数,将CameraImage的plane组合为Uint8List格式
  684. Uint8List _concatenatePlanes(List<Plane> planes) {
  685. final WriteBuffer allBytes = WriteBuffer();
  686. for (Plane plane in planes) {
  687. allBytes.putUint8List(plane.bytes);
  688. }
  689. return allBytes.done().buffer.asUint8List();
  690. }
  691. InputImageFormat _getInputImageFormat(ImageFormatGroup format) {
  692. switch (format) {
  693. case ImageFormatGroup.yuv420:
  694. return InputImageFormat.yuv420;
  695. case ImageFormatGroup.bgra8888:
  696. return InputImageFormat.bgra8888;
  697. default:
  698. throw ArgumentError('Invalid image format');
  699. }
  700. }
  701. /// 销毁检测器
  702. void closeDetector() {
  703. if (_detectionTimer != null) {
  704. state.isShowFaceRecognitionResult = false;
  705. _detectionTimer!.cancel();
  706. _detectionTimer = null;
  707. }
  708. faceDetector.close();
  709. }
  710. }
  711. class FaceRecognitionResult {
  712. bool success;
  713. /// 身份证号
  714. String cardNo;
  715. /// 姓名
  716. String name;
  717. /// 性别
  718. GenderEnum gender;
  719. /// 民族
  720. String nation;
  721. /// 出生日期
  722. DateTime birthday;
  723. /// 地址
  724. String address;
  725. FaceRecognitionResult({
  726. required this.success,
  727. required this.cardNo,
  728. required this.name,
  729. required this.gender,
  730. required this.nation,
  731. required this.birthday,
  732. required this.address,
  733. });
  734. }
  735. /// 运行模式
  736. enum FacialRecognitionMode {
  737. /// 人脸识别(用于登录)
  738. faceRecognition,
  739. /// 人脸录入
  740. faceInput,
  741. }