controller.dart 24 KB

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