controller.dart 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799
  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. DeviceOrientation lockedCaptureOrientation =
  217. controller.value.lockedCaptureOrientation ??
  218. DeviceOrientation.landscapeLeft;
  219. // 根据设备方向调整摄像头角度
  220. int angle = 0;
  221. switch (deviceOrientation) {
  222. case DeviceOrientation.portraitUp:
  223. angle = sensorOrientation;
  224. break;
  225. case DeviceOrientation.landscapeLeft:
  226. angle = sensorOrientation - 90;
  227. break;
  228. case DeviceOrientation.portraitDown:
  229. angle = sensorOrientation - 180;
  230. break;
  231. case DeviceOrientation.landscapeRight:
  232. angle = sensorOrientation + 90;
  233. break;
  234. }
  235. switch (lockedCaptureOrientation) {
  236. case DeviceOrientation.portraitUp:
  237. break;
  238. case DeviceOrientation.landscapeLeft:
  239. angle -= 90;
  240. break;
  241. case DeviceOrientation.portraitDown:
  242. angle -= 180;
  243. break;
  244. case DeviceOrientation.landscapeRight:
  245. angle += 90;
  246. break;
  247. }
  248. /// 前置需要倒转180度
  249. if (cameraDescription.lensDirection == CameraLensDirection.front) {
  250. angle -= 180;
  251. }
  252. /// 体检工作站返回的是后置,但是实际上效果需要转180度
  253. if (_isLocalStation) {
  254. angle -= 180;
  255. }
  256. return angle;
  257. }
  258. /// 相机锁定旋转
  259. Future<void> lockCaptureOrientation() async {
  260. final CameraController? cameraController = kCameraController;
  261. if (cameraController == null || !cameraController.value.isInitialized) {
  262. PromptBox.toast('Error: select a camera first.');
  263. return;
  264. }
  265. if (!cameraController.value.isCaptureOrientationLocked) {
  266. try {
  267. // await cameraController.lockCaptureOrientation(DeviceOrientation.landscapeLeft)
  268. await cameraController.lockCaptureOrientation(_isLocalStation
  269. ? DeviceOrientation.portraitUp
  270. : cameraController.value.deviceOrientation);
  271. } on CameraException catch (e) {
  272. PromptBox.toast('Error: ${e.code}\n${e.description}');
  273. }
  274. } else {
  275. PromptBox.toast('Rotation lock is already enabled.');
  276. }
  277. }
  278. /// 执行一次拍摄
  279. Future<XFile?> takePicture() async {
  280. final CameraController? cameraController = kCameraController;
  281. if (cameraController == null || !cameraController.value.isInitialized) {
  282. PromptBox.toast('Error: select a camera first.');
  283. return null;
  284. }
  285. if (cameraController.value.isTakingPicture) {
  286. // A capture is already pending, do nothing.
  287. return null;
  288. }
  289. try {
  290. final XFile file = await cameraController.takePicture();
  291. return file;
  292. } on CameraException catch (e) {
  293. PromptBox.toast('Error: ${e.code}\n${e.description}');
  294. return null;
  295. }
  296. }
  297. /// 测试图像文件缓存,print 遍历输出
  298. void debugShowCache() async {
  299. final cacheManager = Get.find<ICacheManager>();
  300. double cacheSize = await cacheManager.getCacheSize();
  301. double imageCacheSize = await cacheManager.getImageCacheSize();
  302. debugPrint('cacheSize = $cacheSize : ${formatSize(cacheSize)}');
  303. debugPrint(
  304. 'imageCacheSize = $imageCacheSize : ${formatSize(imageCacheSize)}');
  305. }
  306. /// 文件大小转为可读 Str
  307. static String formatSize(double value) {
  308. List<String> unitArr = ['B', 'K', 'M', 'G'];
  309. int index = 0;
  310. while (value > 1024) {
  311. index++;
  312. value = value / 1024;
  313. }
  314. String size = value.toStringAsFixed(2);
  315. return size + unitArr[index];
  316. }
  317. /// 保存到相册
  318. void saveImageToGallery(XFile image) async {
  319. // 获取图像的字节数据
  320. Uint8List bytes = await image.readAsBytes();
  321. // 将图像保存到相册
  322. await ImageGallerySaver.saveImage(bytes, quality: 100);
  323. }
  324. /// 处理图像裁切
  325. Future<String> clipLocalImage(XFile soureceImage, double scale) async {
  326. assert(scale >= 1, 'scale must be greater than 1');
  327. // 获取图像的字节数据
  328. Uint8List bytes = await soureceImage.readAsBytes();
  329. var codec = await ui.instantiateImageCodec(bytes);
  330. var nextFrame = await codec.getNextFrame();
  331. var image = nextFrame.image;
  332. Rect src = Rect.fromLTWH(
  333. (scale - 1) / 2 / scale * image.width.toDouble(),
  334. (scale - 1) / 2 / scale * image.height.toDouble(),
  335. image.width.toDouble() / scale,
  336. image.height.toDouble() / scale,
  337. );
  338. Rect dst =
  339. Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble());
  340. ui.Image croppedImage = await getCroppedImage(image, src, dst);
  341. ByteData? newImageBytes =
  342. await croppedImage.toByteData(format: ui.ImageByteFormat.png);
  343. if (newImageBytes == null) {
  344. return '';
  345. }
  346. Uint8List newImageUint8List = newImageBytes.buffer.asUint8List();
  347. /// FIXME 不要存到相册而是存到临时目录
  348. Map<Object?, Object?> result =
  349. await ImageGallerySaver.saveImage(newImageUint8List, quality: 100);
  350. String jsonString = jsonEncode(result);
  351. Map<String, dynamic> json = jsonDecode(jsonString);
  352. String? filePath = json['filePath'];
  353. return filePath ?? '';
  354. }
  355. /// 获取图像文件的图像尺寸
  356. Future<Size> getImageSize(XFile soureceImage) async {
  357. // 获取图像的字节数据
  358. Uint8List bytes = await soureceImage.readAsBytes();
  359. var codec = await ui.instantiateImageCodec(bytes);
  360. var nextFrame = await codec.getNextFrame();
  361. var image = nextFrame.image;
  362. return Size(image.width.toDouble(), image.height.toDouble());
  363. }
  364. /// 获取裁切后的图像
  365. Future<ui.Image> getCroppedImage(ui.Image image, Rect src, Rect dst) {
  366. var pictureRecorder = ui.PictureRecorder();
  367. Canvas canvas = Canvas(pictureRecorder);
  368. canvas.drawImageRect(image, src, dst, Paint());
  369. return pictureRecorder.endRecording().toImage(
  370. dst.width.floor(),
  371. dst.height.floor(),
  372. );
  373. }
  374. /// 发生开始人脸识别/人像采集事件
  375. void onCaptureFaceButtonPressed() {
  376. if (mode == FacialRecognitionMode.faceRecognition) {
  377. doFacialRecognitionTimes = 0;
  378. state.isRunningFaceRecognition = true;
  379. doFacialRecognition(); // 人脸识别
  380. } else {
  381. doFaceInput(); // 人像采集
  382. }
  383. }
  384. /// 人脸识别执行次数
  385. int doFacialRecognitionTimes = 0;
  386. /// 人脸识别逻辑
  387. void doFacialRecognition() async {
  388. logger.i("doFacialRecognition 进行人脸识别 $doFacialRecognitionTimes");
  389. doFacialRecognitionTimes++;
  390. if (doFacialRecognitionTimes >= 10) {
  391. // 尝试十次后宣告失败
  392. state.isRunningFaceRecognition = false;
  393. PromptBox.toast('人脸识别失败,请先录入人脸信息或更新人脸信息');
  394. return;
  395. }
  396. if (kCameraController == null) {
  397. state.isRunningFaceRecognition = false;
  398. return;
  399. }
  400. final XFile? file = await takePicture();
  401. state.processingImageLocalPath = '';
  402. if (file != null) {
  403. faceDetector = FaceDetector(options: FaceDetectorOptions());
  404. // faceDetector =
  405. // FaceDetector(options: FaceDetectorOptions(enableContours: true));
  406. int faceNum =
  407. await doDetection(faceDetector, file.path); // max 分辨率下检测用时大约 100ms
  408. if (doFacialRecognitionTimes >= 10) {
  409. // 手动取消,中途跳出循环
  410. state.isRunningFaceRecognition = false;
  411. return;
  412. }
  413. if (faceNum == 0) {
  414. PromptBox.toast('请将面部保持在识别框内');
  415. await Future.delayed(const Duration(seconds: 2));
  416. } else if (faceNum > 1) {
  417. PromptBox.toast('请保持只有一张面部在识别范围内');
  418. await Future.delayed(const Duration(seconds: 2));
  419. } else {
  420. final String fileType = file.path.split('.').last;
  421. if (!['png', 'jpg'].contains(fileType)) {
  422. PromptBox.toast('上传的图像类型错误');
  423. return;
  424. }
  425. state.processingImageLocalPath = file.path;
  426. final url = await rpc.storage.upload(
  427. file,
  428. fileType: fileType,
  429. );
  430. print('⭐⭐⭐⭐⭐⭐⭐⭐ url: $url');
  431. try {
  432. if (url == null || url.isEmpty) {
  433. PromptBox.toast('上传失败');
  434. throw Exception('图像上传超时');
  435. }
  436. PatientBaseDTO result =
  437. await rpc.vitalPatient.getPatientBaseByFaceImageAsync(
  438. GetPatientBaseByFaceImageRequest(
  439. token: Store.user.token,
  440. image: url,
  441. ),
  442. );
  443. state.processingImageLocalPath = '';
  444. if (doFacialRecognitionTimes >= 10) {
  445. // 手动取消,中途跳出循环
  446. state.isRunningFaceRecognition = false;
  447. return;
  448. }
  449. if (result.faceScanErrorType == FaceScanErrorTypeEnum.Success) {
  450. finishFaceDetection(result);
  451. // 识别成功,阻断循环
  452. doFacialRecognitionTimes = 10;
  453. } else if (result.faceScanErrorType ==
  454. FaceScanErrorTypeEnum.NoCreated) {
  455. PromptBox.toast('识别到未采集过的人脸信息');
  456. /// 如果返回结果告知不在档,则快进到失败十次提示重新建档
  457. doFacialRecognitionTimes = 10;
  458. } else {
  459. if (result.errorMessage.isNotNullOrEmpty) {
  460. logger.e(result.errorMessage!);
  461. }
  462. if (kDebugMode) {
  463. PromptBox.toast('无法识别面部信息,请确保面部清晰可见: $doFacialRecognitionTimes');
  464. } else {
  465. PromptBox.toast('无法识别面部信息,请确保面部清晰可见');
  466. }
  467. await Future.delayed(const Duration(seconds: 1));
  468. }
  469. } catch (e) {
  470. logger.e("getPatientBaseByFaceImageAsync failed: $e", e);
  471. state.processingImageLocalPath = '';
  472. }
  473. state.processingImageLocalPath = '';
  474. }
  475. }
  476. if (doFacialRecognitionTimes >= 10) {
  477. // 手动取消,中途跳出循环
  478. state.isRunningFaceRecognition = false;
  479. return;
  480. }
  481. doFacialRecognition();
  482. }
  483. /// 人像采集逻辑
  484. void doFaceInput() async {
  485. if (kCameraController == null) {
  486. return;
  487. }
  488. if (patientInfo != null) {
  489. idCardInfo.idCardNumber = patientInfo!.cardNo!;
  490. if (patientInfo?.photos?.isNotEmpty ?? false) {
  491. ///已经绑定则解绑
  492. await rpc.vitalPatient.unbindByCardNoAsync(UnbindByCardNoRequest(
  493. token: Store.user.token,
  494. cardNo: patientInfo!.cardNo!,
  495. ));
  496. }
  497. }
  498. final XFile? file = await takePicture();
  499. state.processingImageLocalPath = '';
  500. if (file != null) {
  501. faceDetector = FaceDetector(options: FaceDetectorOptions());
  502. // faceDetector =
  503. // FaceDetector(options: FaceDetectorOptions(enableContours: true));
  504. int faceNum =
  505. await doDetection(faceDetector, file.path); // max 分辨率下检测用时大约 100ms
  506. if (faceNum == 0) {
  507. PromptBox.toast('请将面部保持在识别框内');
  508. return;
  509. } else if (faceNum > 1) {
  510. PromptBox.toast('请保持只有一张面部在识别范围内');
  511. return;
  512. }
  513. final String fileType = file.path.split('.').last;
  514. if (!['png', 'jpg'].contains(fileType)) {
  515. PromptBox.toast('上传的图像类型错误');
  516. return;
  517. }
  518. state.processingImageLocalPath = file.path;
  519. final url = await rpc.storage.upload(
  520. file,
  521. fileType: fileType,
  522. );
  523. print('⭐⭐⭐⭐⭐⭐⭐⭐ url: $url');
  524. try {
  525. if (url == null || url.isEmpty) {
  526. PromptBox.toast('上传失败');
  527. throw Exception('图像上传超时');
  528. }
  529. SavePersonDTO result =
  530. await rpc.vitalPatient.savePatientBaseByFaceImageAsync(
  531. SavePatientBaseByFaceImageRequest(
  532. cardNo: idCardInfo.idCardNumber,
  533. token: Store.user.token,
  534. image: url,
  535. ),
  536. );
  537. state.processingImageLocalPath = '';
  538. if (result.success) {
  539. Get.back(result: true);
  540. } else {
  541. // 如果失败且存在 bindCardNo ,则说明已录入过
  542. if (result.bindCardNo != null) {
  543. if (result.bindCardNo == idCardInfo.idCardNumber) {
  544. Get.back(result: true);
  545. } else {
  546. /// 询问是否需要解绑原身份证并绑定当前身份证
  547. /// 原身份证:result.bindCardNo
  548. /// 当前身份证:idCardInfo.idCardNumber
  549. bool? dialogResult = await Get.dialog<bool>(
  550. VAlertDialog(
  551. title: '提示',
  552. width: 600,
  553. content: Container(
  554. margin:
  555. const EdgeInsets.only(bottom: 20, left: 20, right: 20),
  556. child: Text(
  557. '该人脸已绑定身份证(${result.bindCardNo})\n是否解绑并绑定当前身份证(${idCardInfo.idCardNumber})?',
  558. style: const TextStyle(fontSize: 20),
  559. textAlign: TextAlign.left,
  560. ),
  561. ),
  562. showCancel: true,
  563. onConfirm: () async {
  564. bool success = await unbindAndCreateByFaceImageAsync(
  565. result.bindCardNo!,
  566. idCardInfo.idCardNumber,
  567. url,
  568. );
  569. if (success) {
  570. Get.back(result: true);
  571. } else {
  572. Get.back(result: false);
  573. }
  574. },
  575. ),
  576. );
  577. if (dialogResult != null && dialogResult) {
  578. Get.back(result: true);
  579. } else if (dialogResult != null && !dialogResult) {
  580. PromptBox.toast('人脸数据存入失败');
  581. } else {
  582. PromptBox.toast('人脸数据存入取消');
  583. }
  584. }
  585. } else {
  586. PromptBox.toast('人脸数据存入失败: ${result.errMessage}');
  587. }
  588. }
  589. } catch (e) {
  590. state.processingImageLocalPath = '';
  591. logger.e("savePatientBaseByFaceImageAsync failed: $e", e);
  592. }
  593. state.processingImageLocalPath = '';
  594. }
  595. }
  596. /// 取消采集
  597. void doCancelCapture() {
  598. doFacialRecognitionTimes = 10;
  599. state.isRunningFaceRecognition = false;
  600. }
  601. /// 重新绑定并创建新档案
  602. Future<bool> unbindAndCreateByFaceImageAsync(
  603. String oldId, String newId, String url) async {
  604. DeletePersonDTO result =
  605. await rpc.vitalPatient.unbindAndCreateByFaceImageAsync(
  606. UnbindAndCreateByFaceImageRequest(
  607. oldCardNo: oldId,
  608. newCardNo: newId,
  609. token: Store.user.token,
  610. image: url,
  611. ),
  612. );
  613. return result.success;
  614. }
  615. /// 在 widget 内存中分配后立即调用。
  616. @override
  617. void onInit() async {
  618. // await initCamera();
  619. super.onInit();
  620. WidgetsBinding.instance.addObserver(this);
  621. }
  622. /// 在 onInit() 之后调用 1 帧。这是进入的理想场所
  623. @override
  624. void onReady() async {
  625. super.onReady();
  626. logger.i(
  627. "onReady 进入人脸识别/采集页面,当前模式:${mode == FacialRecognitionMode.faceRecognition ? '人脸识别' : '人像采集'}");
  628. await initAvailableCameras();
  629. /// 如果只有一个摄像头,则直接打开
  630. if (_cameras.length == 1) {
  631. await openNewCamera(_cameras.first);
  632. lockCaptureOrientation();
  633. update();
  634. state.isCameraReady = true;
  635. return;
  636. }
  637. if (state.isUsingFrontCamera) {
  638. openFrontCamera();
  639. } else {
  640. openBackCamera();
  641. }
  642. }
  643. /// 在 [onDelete] 方法之前调用。
  644. @override
  645. void onClose() {
  646. logger.i("onClose 离开人脸识别/采集页面");
  647. super.onClose();
  648. // 关闭人脸采集
  649. doCancelCapture();
  650. final cacheManager = Get.find<ICacheManager>();
  651. cacheManager.clearApplicationImageCache();
  652. closeDetector();
  653. WidgetsBinding.instance.removeObserver(this);
  654. final CameraController? cameraController = kCameraController;
  655. if (cameraController != null) {
  656. kCameraController = null;
  657. cameraController.dispose();
  658. }
  659. }
  660. /// dispose 释放内存
  661. @override
  662. void dispose() {
  663. super.dispose();
  664. }
  665. @override
  666. void didChangeAppLifecycleState(AppLifecycleState state) async {
  667. super.didChangeAppLifecycleState(state);
  668. final CameraController? cameraController = kCameraController;
  669. // App state changed before we got the chance to initialize.
  670. if (cameraController == null || !cameraController.value.isInitialized) {
  671. return;
  672. }
  673. if (state == AppLifecycleState.inactive) {
  674. cameraController.dispose();
  675. this.state.isCameraReady = false;
  676. } else if (state == AppLifecycleState.resumed) {
  677. await openNewCamera(cameraController.description);
  678. this.state.isCameraReady = true;
  679. }
  680. }
  681. /// 完成人脸识别
  682. void finishFaceDetection(PatientBaseDTO patient) {
  683. Get.back<FaceRecognitionResult>(
  684. result: FaceRecognitionResult(
  685. success: true,
  686. patientInfo: patient,
  687. ),
  688. );
  689. }
  690. /// 面部识别 基于 Google's ML Kit
  691. InputImage inputImage = InputImage.fromFilePath('');
  692. FaceDetector faceDetector = FaceDetector(options: FaceDetectorOptions());
  693. // 进行一次人脸检测 (返回人脸数量)
  694. Future<int> doDetection(
  695. FaceDetector faceDetector,
  696. String imagePath,
  697. ) async {
  698. inputImage = InputImage.fromFilePath(imagePath);
  699. // inputImage = image;
  700. final List<Face> faces = await faceDetector.processImage(inputImage);
  701. kFrameFacesResult = [];
  702. kFrameFacesResult.addAll(faces);
  703. return kFrameFacesResult.length;
  704. }
  705. /// 销毁检测器
  706. void closeDetector() {
  707. faceDetector.close();
  708. }
  709. }
  710. class FaceRecognitionResult {
  711. bool success;
  712. /// 身份信息
  713. PatientBaseDTO patientInfo;
  714. FaceRecognitionResult({
  715. required this.success,
  716. required this.patientInfo,
  717. });
  718. }
  719. /// 运行模式
  720. enum FacialRecognitionMode {
  721. /// 人脸识别(用于登录)
  722. faceRecognition,
  723. /// 人像采集
  724. faceInput,
  725. }