controller.dart 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725
  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/components/alert_dialog.dart';
  14. import 'package:vitalapp/managers/interfaces/cache.dart';
  15. import 'package:fis_common/logger/logger.dart';
  16. import 'package:vitalapp/rpc.dart';
  17. import 'package:vitalapp/store/store.dart';
  18. import 'dart:ui' as ui;
  19. import 'index.dart';
  20. import 'package:fis_common/index.dart';
  21. class FacialRecognitionController extends GetxController
  22. with WidgetsBindingObserver {
  23. FacialRecognitionController({
  24. required this.mode,
  25. this.patientInfo,
  26. });
  27. final FacialRecognitionMode mode;
  28. /// 当前需要录入的身份信息
  29. final PatientDTO? patientInfo;
  30. final state = FacialRecognitionState();
  31. List<CameraDescription> _cameras = <CameraDescription>[];
  32. List<CameraDescription> get cameras => _cameras;
  33. CameraController? kCameraController;
  34. double _minAvailableExposureOffset = 0.0;
  35. double _maxAvailableExposureOffset = 0.0;
  36. double _minAvailableZoom = 1.0;
  37. double _maxAvailableZoom = 1.0;
  38. final double _currentScale = 1.0;
  39. double _baseScale = 1.0;
  40. // 屏幕上手指数量
  41. int pointers = 0;
  42. // 当前需要返回的身份信息
  43. IdCardInfoModel idCardInfo = IdCardInfoModel();
  44. /// 开始缩放
  45. void handleScaleStart(ScaleStartDetails details) {
  46. _baseScale = _currentScale;
  47. }
  48. /// 当前捕获帧的人脸列表
  49. List<Face> kFrameFacesResult = [];
  50. /// 当前捕获帧大小
  51. Size kFrameImageSize = Size.zero;
  52. /// 缩放更新
  53. Future<void> handleScaleUpdate(ScaleUpdateDetails details) async {
  54. // When there are not exactly two fingers on screen don't scale
  55. if (kCameraController == null || pointers != 2) {
  56. return;
  57. }
  58. // 屏蔽缩放
  59. // _currentScale = (_baseScale * details.scale)
  60. // .clamp(_minAvailableZoom, _maxAvailableZoom);
  61. // await kCameraController!.setZoomLevel(_currentScale);
  62. }
  63. /// 修改对焦点 [暂不执行]
  64. void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) {
  65. if (kCameraController == null) {
  66. return;
  67. }
  68. final CameraController cameraController = kCameraController!;
  69. final Offset offset = Offset(
  70. details.localPosition.dx / constraints.maxWidth,
  71. details.localPosition.dy / constraints.maxHeight,
  72. );
  73. cameraController.setExposurePoint(offset);
  74. cameraController.setFocusPoint(offset);
  75. }
  76. /// 初始化相机
  77. Future<void> initAvailableCameras() async {
  78. try {
  79. _cameras = await availableCameras();
  80. if (_cameras.isNotEmpty) {
  81. // state.isCameraReady = true;
  82. }
  83. // print("cameras: ${_cameras.length}");
  84. } on CameraException catch (e) {
  85. logger.e("cameras: ${e.code} ${e.description}");
  86. }
  87. }
  88. /// 启动指定相机
  89. Future<void> openNewCamera(CameraDescription cameraDescription) async {
  90. final CameraController? oldController = kCameraController;
  91. if (oldController != null) {
  92. // `kCameraController` needs to be set to null before getting disposed,
  93. // to avoid a race condition when we use the kCameraController that is being
  94. // disposed. This happens when camera permission dialog shows up,
  95. // which triggers `didChangeAppLifecycleState`, which disposes and
  96. // re-creates the kCameraController.
  97. kCameraController = null;
  98. await oldController.dispose();
  99. }
  100. final CameraController cameraController = CameraController(
  101. cameraDescription,
  102. ResolutionPreset.max,
  103. enableAudio: false,
  104. imageFormatGroup: ImageFormatGroup.jpeg,
  105. );
  106. kCameraController = cameraController;
  107. // If the kCameraController is updated then update the UI.
  108. cameraController.addListener(() {
  109. if (cameraController.value.hasError) {
  110. PromptBox.toast(
  111. "Camera error ${cameraController.value.errorDescription}");
  112. }
  113. });
  114. try {
  115. await cameraController.initialize();
  116. await Future.wait(<Future<Object?>>[
  117. // The exposure mode is currently not supported on the web.
  118. ...!kIsWeb
  119. ? <Future<Object?>>[
  120. cameraController.getMinExposureOffset().then(
  121. (double value) => _minAvailableExposureOffset = value),
  122. cameraController
  123. .getMaxExposureOffset()
  124. .then((double value) => _maxAvailableExposureOffset = value)
  125. ]
  126. : <Future<Object?>>[],
  127. cameraController
  128. .getMaxZoomLevel()
  129. .then((double value) => _maxAvailableZoom = value),
  130. cameraController
  131. .getMinZoomLevel()
  132. .then((double value) => _minAvailableZoom = value),
  133. ]);
  134. } on CameraException catch (e) {
  135. switch (e.code) {
  136. case 'CameraAccessDenied':
  137. PromptBox.toast('You have denied camera access.');
  138. break;
  139. case 'CameraAccessDeniedWithoutPrompt':
  140. // iOS only
  141. PromptBox.toast('Please go to Settings app to enable camera access.');
  142. break;
  143. case 'CameraAccessRestricted':
  144. // iOS only
  145. PromptBox.toast('Camera access is restricted.');
  146. break;
  147. case 'AudioAccessDenied':
  148. PromptBox.toast('You have denied audio access.');
  149. break;
  150. case 'AudioAccessDeniedWithoutPrompt':
  151. // iOS only
  152. PromptBox.toast('Please go to Settings app to enable audio access.');
  153. break;
  154. case 'AudioAccessRestricted':
  155. // iOS only
  156. PromptBox.toast('Audio access is restricted.');
  157. break;
  158. default:
  159. PromptBox.toast('Error: ${e.code}\n${e.description}');
  160. break;
  161. }
  162. }
  163. }
  164. /// 遍历当前相机列表并启动后置相机
  165. void openBackCamera() async {
  166. logger.i("openBackCamera 启动前置摄像头");
  167. if (_cameras.isEmpty) {
  168. PromptBox.toast('Error: No cameras found.');
  169. } else {
  170. for (CameraDescription cameraDescription in _cameras) {
  171. if (cameraDescription.lensDirection == CameraLensDirection.back) {
  172. await openNewCamera(cameraDescription);
  173. lockCaptureOrientation();
  174. state.isUsingFrontCamera = false;
  175. update();
  176. state.isCameraReady = true;
  177. break;
  178. }
  179. }
  180. }
  181. }
  182. /// 遍历当前相机列表并启动前置相机
  183. void openFrontCamera() async {
  184. logger.i("openFrontCamera 启动后置摄像头");
  185. if (_cameras.isEmpty) {
  186. PromptBox.toast('Error: No cameras found.');
  187. } else {
  188. for (CameraDescription cameraDescription in _cameras) {
  189. if (cameraDescription.lensDirection == CameraLensDirection.front) {
  190. await openNewCamera(cameraDescription);
  191. state.isUsingFrontCamera = true;
  192. lockCaptureOrientation();
  193. update();
  194. state.isCameraReady = true;
  195. break;
  196. }
  197. }
  198. }
  199. }
  200. /// 前后置摄像头切换
  201. void switchCameraLens() async {
  202. if (state.isUsingFrontCamera) {
  203. openBackCamera();
  204. } else {
  205. openFrontCamera();
  206. }
  207. }
  208. /// 相机锁定旋转
  209. Future<void> lockCaptureOrientation() async {
  210. final CameraController? cameraController = kCameraController;
  211. if (cameraController == null || !cameraController.value.isInitialized) {
  212. PromptBox.toast('Error: select a camera first.');
  213. return;
  214. }
  215. if (!cameraController.value.isCaptureOrientationLocked) {
  216. try {
  217. await cameraController
  218. .lockCaptureOrientation(DeviceOrientation.landscapeLeft);
  219. } on CameraException catch (e) {
  220. PromptBox.toast('Error: ${e.code}\n${e.description}');
  221. }
  222. } else {
  223. PromptBox.toast('Rotation lock is already enabled.');
  224. }
  225. }
  226. /// 执行一次拍摄
  227. Future<XFile?> takePicture() async {
  228. final CameraController? cameraController = kCameraController;
  229. if (cameraController == null || !cameraController.value.isInitialized) {
  230. PromptBox.toast('Error: select a camera first.');
  231. return null;
  232. }
  233. if (cameraController.value.isTakingPicture) {
  234. // A capture is already pending, do nothing.
  235. return null;
  236. }
  237. try {
  238. final XFile file = await cameraController.takePicture();
  239. return file;
  240. } on CameraException catch (e) {
  241. PromptBox.toast('Error: ${e.code}\n${e.description}');
  242. return null;
  243. }
  244. }
  245. /// 测试图像文件缓存,print 遍历输出
  246. void debugShowCache() async {
  247. final cacheManager = Get.find<ICacheManager>();
  248. double cacheSize = await cacheManager.getCacheSize();
  249. double imageCacheSize = await cacheManager.getImageCacheSize();
  250. debugPrint('cacheSize = $cacheSize : ${formatSize(cacheSize)}');
  251. debugPrint(
  252. 'imageCacheSize = $imageCacheSize : ${formatSize(imageCacheSize)}');
  253. }
  254. /// 文件大小转为可读 Str
  255. static String formatSize(double value) {
  256. List<String> unitArr = ['B', 'K', 'M', 'G'];
  257. int index = 0;
  258. while (value > 1024) {
  259. index++;
  260. value = value / 1024;
  261. }
  262. String size = value.toStringAsFixed(2);
  263. return size + unitArr[index];
  264. }
  265. /// 保存到相册
  266. void saveImageToGallery(XFile image) async {
  267. // 获取图像的字节数据
  268. Uint8List bytes = await image.readAsBytes();
  269. // 将图像保存到相册
  270. await ImageGallerySaver.saveImage(bytes, quality: 100);
  271. }
  272. /// 处理图像裁切
  273. Future<String> clipLocalImage(XFile soureceImage, double scale) async {
  274. assert(scale >= 1, 'scale must be greater than 1');
  275. // 获取图像的字节数据
  276. Uint8List bytes = await soureceImage.readAsBytes();
  277. var codec = await ui.instantiateImageCodec(bytes);
  278. var nextFrame = await codec.getNextFrame();
  279. var image = nextFrame.image;
  280. Rect src = Rect.fromLTWH(
  281. (scale - 1) / 2 / scale * image.width.toDouble(),
  282. (scale - 1) / 2 / scale * image.height.toDouble(),
  283. image.width.toDouble() / scale,
  284. image.height.toDouble() / scale,
  285. );
  286. Rect dst =
  287. Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble());
  288. ui.Image croppedImage = await getCroppedImage(image, src, dst);
  289. ByteData? newImageBytes =
  290. await croppedImage.toByteData(format: ui.ImageByteFormat.png);
  291. if (newImageBytes == null) {
  292. return '';
  293. }
  294. Uint8List newImageUint8List = newImageBytes.buffer.asUint8List();
  295. /// FIXME 不要存到相册而是存到临时目录
  296. Map<Object?, Object?> result =
  297. await ImageGallerySaver.saveImage(newImageUint8List, quality: 100);
  298. String jsonString = jsonEncode(result);
  299. Map<String, dynamic> json = jsonDecode(jsonString);
  300. String? filePath = json['filePath'];
  301. return filePath ?? '';
  302. }
  303. /// 获取图像文件的图像尺寸
  304. Future<Size> getImageSize(XFile soureceImage) async {
  305. // 获取图像的字节数据
  306. Uint8List bytes = await soureceImage.readAsBytes();
  307. var codec = await ui.instantiateImageCodec(bytes);
  308. var nextFrame = await codec.getNextFrame();
  309. var image = nextFrame.image;
  310. return Size(image.width.toDouble(), image.height.toDouble());
  311. }
  312. /// 获取裁切后的图像
  313. Future<ui.Image> getCroppedImage(ui.Image image, Rect src, Rect dst) {
  314. var pictureRecorder = ui.PictureRecorder();
  315. Canvas canvas = Canvas(pictureRecorder);
  316. canvas.drawImageRect(image, src, dst, Paint());
  317. return pictureRecorder.endRecording().toImage(
  318. dst.width.floor(),
  319. dst.height.floor(),
  320. );
  321. }
  322. /// 发生开始人脸识别/人像采集事件
  323. void onCaptureFaceButtonPressed() {
  324. if (mode == FacialRecognitionMode.faceRecognition) {
  325. doFacialRecognitionTimes = 0;
  326. state.isRunningFaceRecognition = true;
  327. doFacialRecognition(); // 人脸识别
  328. } else {
  329. doFaceInput(); // 人像采集
  330. }
  331. }
  332. /// 人脸识别执行次数
  333. int doFacialRecognitionTimes = 0;
  334. /// 人脸识别逻辑
  335. void doFacialRecognition() async {
  336. logger.i("doFacialRecognition 进行人脸识别 $doFacialRecognitionTimes");
  337. doFacialRecognitionTimes++;
  338. if (doFacialRecognitionTimes >= 10) {
  339. // 尝试十次后宣告失败
  340. state.isRunningFaceRecognition = false;
  341. PromptBox.toast('人脸识别失败,请先录入人脸信息或更新人脸信息');
  342. return;
  343. }
  344. if (kCameraController == null) {
  345. state.isRunningFaceRecognition = false;
  346. return;
  347. }
  348. final XFile? file = await takePicture();
  349. state.processingImageLocalPath = '';
  350. if (file != null) {
  351. faceDetector = FaceDetector(options: FaceDetectorOptions());
  352. // faceDetector =
  353. // FaceDetector(options: FaceDetectorOptions(enableContours: true));
  354. int faceNum =
  355. await doDetection(faceDetector, file.path); // max 分辨率下检测用时大约 100ms
  356. if (doFacialRecognitionTimes >= 10) {
  357. // 手动取消,中途跳出循环
  358. state.isRunningFaceRecognition = false;
  359. return;
  360. }
  361. if (faceNum == 0) {
  362. PromptBox.toast('请将面部保持在识别框内');
  363. await Future.delayed(const Duration(seconds: 2));
  364. } else if (faceNum > 1) {
  365. PromptBox.toast('请保持只有一张面部在识别范围内');
  366. await Future.delayed(const Duration(seconds: 2));
  367. } else {
  368. final String fileType = file.path.split('.').last;
  369. if (!['png', 'jpg'].contains(fileType)) {
  370. PromptBox.toast('上传的图像类型错误');
  371. return;
  372. }
  373. state.processingImageLocalPath = file.path;
  374. final url = await rpc.storage.upload(
  375. file,
  376. fileType: fileType,
  377. );
  378. print('⭐⭐⭐⭐⭐⭐⭐⭐ url: $url');
  379. try {
  380. if (url == null || url.isEmpty) {
  381. PromptBox.toast('上传失败');
  382. throw Exception('图像上传超时');
  383. }
  384. PatientBaseDTO result =
  385. await rpc.patient.getPatientBaseByFaceImageAsync(
  386. GetPatientBaseByFaceImageRequest(
  387. token: Store.user.token,
  388. image: url,
  389. ),
  390. );
  391. state.processingImageLocalPath = '';
  392. if (doFacialRecognitionTimes >= 10) {
  393. // 手动取消,中途跳出循环
  394. state.isRunningFaceRecognition = false;
  395. return;
  396. }
  397. if (result.faceScanErrorType == FaceScanErrorTypeEnum.Success) {
  398. finishFaceDetection(result);
  399. // 识别成功,阻断循环
  400. doFacialRecognitionTimes = 10;
  401. } else if (result.faceScanErrorType ==
  402. FaceScanErrorTypeEnum.NoCreated) {
  403. PromptBox.toast('识别到未采集过的人脸信息');
  404. /// 如果返回结果告知不在档,则快进到失败十次提示重新建档
  405. doFacialRecognitionTimes = 10;
  406. } else {
  407. if (result.errorMessage.isNotNullOrEmpty) {
  408. logger.e(result.errorMessage!);
  409. }
  410. if (kDebugMode) {
  411. PromptBox.toast('无法识别面部信息,请确保面部清晰可见: $doFacialRecognitionTimes');
  412. } else {
  413. PromptBox.toast('无法识别面部信息,请确保面部清晰可见');
  414. }
  415. await Future.delayed(const Duration(seconds: 1));
  416. }
  417. } catch (e) {
  418. logger.e("getPatientBaseByFaceImageAsync failed: $e", e);
  419. state.processingImageLocalPath = '';
  420. }
  421. state.processingImageLocalPath = '';
  422. }
  423. }
  424. if (doFacialRecognitionTimes >= 10) {
  425. // 手动取消,中途跳出循环
  426. state.isRunningFaceRecognition = false;
  427. return;
  428. }
  429. doFacialRecognition();
  430. }
  431. /// 人像采集逻辑
  432. void doFaceInput() async {
  433. if (kCameraController == null) {
  434. return;
  435. }
  436. if (patientInfo != null) {
  437. idCardInfo.idCardNumber = patientInfo!.cardNo!;
  438. }
  439. final XFile? file = await takePicture();
  440. state.processingImageLocalPath = '';
  441. if (file != null) {
  442. faceDetector = FaceDetector(options: FaceDetectorOptions());
  443. // faceDetector =
  444. // FaceDetector(options: FaceDetectorOptions(enableContours: true));
  445. int faceNum =
  446. await doDetection(faceDetector, file.path); // max 分辨率下检测用时大约 100ms
  447. if (faceNum == 0) {
  448. PromptBox.toast('请将面部保持在识别框内');
  449. return;
  450. } else if (faceNum > 1) {
  451. PromptBox.toast('请保持只有一张面部在识别范围内');
  452. return;
  453. }
  454. final String fileType = file.path.split('.').last;
  455. if (!['png', 'jpg'].contains(fileType)) {
  456. PromptBox.toast('上传的图像类型错误');
  457. return;
  458. }
  459. state.processingImageLocalPath = file.path;
  460. final url = await rpc.storage.upload(
  461. file,
  462. fileType: fileType,
  463. );
  464. print('⭐⭐⭐⭐⭐⭐⭐⭐ url: $url');
  465. try {
  466. if (url == null || url.isEmpty) {
  467. PromptBox.toast('上传失败');
  468. throw Exception('图像上传超时');
  469. }
  470. SavePersonDTO result =
  471. await rpc.patient.savePatientBaseByFaceImageAsync(
  472. SavePatientBaseByFaceImageRequest(
  473. cardNo: idCardInfo.idCardNumber,
  474. token: Store.user.token,
  475. image: url,
  476. ),
  477. );
  478. state.processingImageLocalPath = '';
  479. if (result.success) {
  480. Get.back(result: true);
  481. } else {
  482. // 如果失败且存在 bindCardNo ,则说明已录入过
  483. if (result.bindCardNo != null) {
  484. if (result.bindCardNo == idCardInfo.idCardNumber) {
  485. Get.back(result: true);
  486. } else {
  487. /// 询问是否需要解绑原身份证并绑定当前身份证
  488. /// 原身份证:result.bindCardNo
  489. /// 当前身份证:idCardInfo.idCardNumber
  490. bool? dialogResult = await Get.dialog<bool>(
  491. VAlertDialog(
  492. title: '提示',
  493. width: 600,
  494. content: Container(
  495. margin:
  496. const EdgeInsets.only(bottom: 20, left: 20, right: 20),
  497. child: Text(
  498. '该人脸已绑定身份证(${result.bindCardNo})\n是否解绑并绑定当前身份证(${idCardInfo.idCardNumber})?',
  499. style: const TextStyle(fontSize: 20),
  500. textAlign: TextAlign.left,
  501. ),
  502. ),
  503. showCancel: true,
  504. onConfirm: () async {
  505. bool success = await unbindAndCreateByFaceImageAsync(
  506. result.bindCardNo!,
  507. idCardInfo.idCardNumber,
  508. url,
  509. );
  510. if (success) {
  511. Get.back(result: true);
  512. } else {
  513. Get.back(result: false);
  514. }
  515. },
  516. ),
  517. );
  518. if (dialogResult != null && dialogResult) {
  519. Get.back(result: true);
  520. } else if (dialogResult != null && !dialogResult) {
  521. PromptBox.toast('人脸数据存入失败');
  522. } else {
  523. PromptBox.toast('人脸数据存入取消');
  524. }
  525. }
  526. } else {
  527. PromptBox.toast('人脸数据存入失败: ${result.errMessage}');
  528. }
  529. }
  530. } catch (e) {
  531. state.processingImageLocalPath = '';
  532. logger.e("savePatientBaseByFaceImageAsync failed: $e", e);
  533. }
  534. state.processingImageLocalPath = '';
  535. }
  536. }
  537. /// 取消采集
  538. void doCancelCapture() {
  539. doFacialRecognitionTimes = 10;
  540. state.isRunningFaceRecognition = false;
  541. }
  542. /// 重新绑定并创建新档案
  543. Future<bool> unbindAndCreateByFaceImageAsync(
  544. String oldId, String newId, String url) async {
  545. DeletePersonDTO result = await rpc.patient.unbindAndCreateByFaceImageAsync(
  546. UnbindAndCreateByFaceImageRequest(
  547. oldCardNo: oldId,
  548. newCardNo: newId,
  549. token: Store.user.token,
  550. image: url,
  551. ),
  552. );
  553. return result.success;
  554. }
  555. /// 在 widget 内存中分配后立即调用。
  556. @override
  557. void onInit() async {
  558. // await initCamera();
  559. super.onInit();
  560. WidgetsBinding.instance.addObserver(this);
  561. }
  562. /// 在 onInit() 之后调用 1 帧。这是进入的理想场所
  563. @override
  564. void onReady() async {
  565. super.onReady();
  566. logger.i(
  567. "onReady 进入人脸识别/采集页面,当前模式:${mode == FacialRecognitionMode.faceRecognition ? '人脸识别' : '人像采集'}");
  568. await initAvailableCameras();
  569. if (state.isUsingFrontCamera) {
  570. openFrontCamera();
  571. } else {
  572. openBackCamera();
  573. }
  574. }
  575. /// 在 [onDelete] 方法之前调用。
  576. @override
  577. void onClose() {
  578. logger.i("onClose 离开人脸识别/采集页面");
  579. super.onClose();
  580. // 关闭人脸采集
  581. doCancelCapture();
  582. final cacheManager = Get.find<ICacheManager>();
  583. cacheManager.clearApplicationImageCache();
  584. closeDetector();
  585. WidgetsBinding.instance.removeObserver(this);
  586. final CameraController? cameraController = kCameraController;
  587. if (cameraController != null) {
  588. kCameraController = null;
  589. cameraController.dispose();
  590. }
  591. }
  592. /// dispose 释放内存
  593. @override
  594. void dispose() {
  595. super.dispose();
  596. }
  597. @override
  598. void didChangeAppLifecycleState(AppLifecycleState state) async {
  599. super.didChangeAppLifecycleState(state);
  600. final CameraController? cameraController = kCameraController;
  601. // App state changed before we got the chance to initialize.
  602. if (cameraController == null || !cameraController.value.isInitialized) {
  603. return;
  604. }
  605. if (state == AppLifecycleState.inactive) {
  606. cameraController.dispose();
  607. this.state.isCameraReady = false;
  608. } else if (state == AppLifecycleState.resumed) {
  609. await openNewCamera(cameraController.description);
  610. this.state.isCameraReady = true;
  611. }
  612. }
  613. /// 完成人脸识别
  614. void finishFaceDetection(PatientBaseDTO patient) {
  615. Get.back<FaceRecognitionResult>(
  616. result: FaceRecognitionResult(
  617. success: true,
  618. patientInfo: patient,
  619. ),
  620. );
  621. }
  622. /// 面部识别 基于 Google's ML Kit
  623. InputImage inputImage = InputImage.fromFilePath('');
  624. FaceDetector faceDetector = FaceDetector(options: FaceDetectorOptions());
  625. // 进行一次人脸检测 (返回人脸数量)
  626. Future<int> doDetection(
  627. FaceDetector faceDetector,
  628. String imagePath,
  629. ) async {
  630. inputImage = InputImage.fromFilePath(imagePath);
  631. // inputImage = image;
  632. final List<Face> faces = await faceDetector.processImage(inputImage);
  633. kFrameFacesResult = [];
  634. kFrameFacesResult.addAll(faces);
  635. return kFrameFacesResult.length;
  636. }
  637. /// 销毁检测器
  638. void closeDetector() {
  639. faceDetector.close();
  640. }
  641. }
  642. class FaceRecognitionResult {
  643. bool success;
  644. /// 身份信息
  645. PatientBaseDTO patientInfo;
  646. FaceRecognitionResult({
  647. required this.success,
  648. required this.patientInfo,
  649. });
  650. }
  651. /// 运行模式
  652. enum FacialRecognitionMode {
  653. /// 人脸识别(用于登录)
  654. faceRecognition,
  655. /// 人像采集
  656. faceInput,
  657. }