controller.dart 25 KB

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