controller.dart 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'dart:io';
  4. import 'dart:math';
  5. import 'package:camera/camera.dart';
  6. import 'package:flutter/foundation.dart';
  7. import 'package:flutter/material.dart';
  8. import 'package:flutter/services.dart';
  9. import 'package:get/get.dart';
  10. import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
  11. import 'package:image_gallery_saver/image_gallery_saver.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 'dart:ui' as ui;
  16. import 'index.dart';
  17. class FacialRecognitionController extends GetxController
  18. with WidgetsBindingObserver {
  19. FacialRecognitionController();
  20. final state = FacialRecognitionState();
  21. List<CameraDescription> _cameras = <CameraDescription>[];
  22. List<CameraDescription> get cameras => _cameras;
  23. CameraController? kCameraController;
  24. double _minAvailableExposureOffset = 0.0;
  25. double _maxAvailableExposureOffset = 0.0;
  26. double _minAvailableZoom = 1.0;
  27. double _maxAvailableZoom = 1.0;
  28. double _currentScale = 1.0;
  29. double _baseScale = 1.0;
  30. // 屏幕上手指数量
  31. int pointers = 0;
  32. // 当前身份证信息
  33. IdCardInfoModel idCardInfo = IdCardInfoModel();
  34. /// 开始缩放
  35. void handleScaleStart(ScaleStartDetails details) {
  36. _baseScale = _currentScale;
  37. }
  38. /// 当前捕获帧的人脸列表
  39. List<Face> kFrameFacesResult = [];
  40. /// 当前捕获帧大小
  41. Size kFrameImageSize = Size.zero;
  42. /// 缩放更新
  43. Future<void> handleScaleUpdate(ScaleUpdateDetails details) async {
  44. // When there are not exactly two fingers on screen don't scale
  45. if (kCameraController == null || pointers != 2) {
  46. return;
  47. }
  48. // 屏蔽缩放
  49. // _currentScale = (_baseScale * details.scale)
  50. // .clamp(_minAvailableZoom, _maxAvailableZoom);
  51. // await kCameraController!.setZoomLevel(_currentScale);
  52. }
  53. /// 修改对焦点 [暂不执行]
  54. void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) {
  55. if (kCameraController == null) {
  56. return;
  57. }
  58. final CameraController cameraController = kCameraController!;
  59. final Offset offset = Offset(
  60. details.localPosition.dx / constraints.maxWidth,
  61. details.localPosition.dy / constraints.maxHeight,
  62. );
  63. cameraController.setExposurePoint(offset);
  64. cameraController.setFocusPoint(offset);
  65. }
  66. /// 初始化相机
  67. Future<void> initAvailableCameras() async {
  68. try {
  69. _cameras = await availableCameras();
  70. if (_cameras.isNotEmpty) {
  71. // state.isCameraReady = true;
  72. }
  73. // print("cameras: ${_cameras.length}");
  74. } on CameraException catch (e) {
  75. logger.e("cameras: ${e.code} ${e.description}");
  76. }
  77. }
  78. /// 启动指定相机
  79. Future<void> onNewCameraSelected(CameraDescription cameraDescription) async {
  80. final CameraController? oldController = kCameraController;
  81. if (oldController != null) {
  82. // `kCameraController` needs to be set to null before getting disposed,
  83. // to avoid a race condition when we use the kCameraController that is being
  84. // disposed. This happens when camera permission dialog shows up,
  85. // which triggers `didChangeAppLifecycleState`, which disposes and
  86. // re-creates the kCameraController.
  87. kCameraController = null;
  88. await oldController.dispose();
  89. }
  90. final CameraController cameraController = CameraController(
  91. cameraDescription,
  92. ResolutionPreset.max,
  93. enableAudio: false,
  94. imageFormatGroup: ImageFormatGroup.jpeg,
  95. );
  96. kCameraController = cameraController;
  97. // If the kCameraController is updated then update the UI.
  98. cameraController.addListener(() {
  99. if (cameraController.value.hasError) {
  100. PromptBox.toast(
  101. "Camera error ${cameraController.value.errorDescription}");
  102. }
  103. });
  104. try {
  105. await cameraController.initialize();
  106. await Future.wait(<Future<Object?>>[
  107. // The exposure mode is currently not supported on the web.
  108. ...!kIsWeb
  109. ? <Future<Object?>>[
  110. cameraController.getMinExposureOffset().then(
  111. (double value) => _minAvailableExposureOffset = value),
  112. cameraController
  113. .getMaxExposureOffset()
  114. .then((double value) => _maxAvailableExposureOffset = value)
  115. ]
  116. : <Future<Object?>>[],
  117. cameraController
  118. .getMaxZoomLevel()
  119. .then((double value) => _maxAvailableZoom = value),
  120. cameraController
  121. .getMinZoomLevel()
  122. .then((double value) => _minAvailableZoom = value),
  123. ]);
  124. } on CameraException catch (e) {
  125. switch (e.code) {
  126. case 'CameraAccessDenied':
  127. PromptBox.toast('You have denied camera access.');
  128. break;
  129. case 'CameraAccessDeniedWithoutPrompt':
  130. // iOS only
  131. PromptBox.toast('Please go to Settings app to enable camera access.');
  132. break;
  133. case 'CameraAccessRestricted':
  134. // iOS only
  135. PromptBox.toast('Camera access is restricted.');
  136. break;
  137. case 'AudioAccessDenied':
  138. PromptBox.toast('You have denied audio access.');
  139. break;
  140. case 'AudioAccessDeniedWithoutPrompt':
  141. // iOS only
  142. PromptBox.toast('Please go to Settings app to enable audio access.');
  143. break;
  144. case 'AudioAccessRestricted':
  145. // iOS only
  146. PromptBox.toast('Audio access is restricted.');
  147. break;
  148. default:
  149. PromptBox.toast('Error: ${e.code}\n${e.description}');
  150. break;
  151. }
  152. }
  153. }
  154. /// 遍历当前相机列表并启动后置相机
  155. void openBackCamera() async {
  156. if (_cameras.isEmpty) {
  157. PromptBox.toast('Error: No cameras found.');
  158. } else {
  159. for (CameraDescription cameraDescription in _cameras) {
  160. if (cameraDescription.lensDirection == CameraLensDirection.back) {
  161. await onNewCameraSelected(cameraDescription);
  162. lockCaptureOrientation();
  163. update();
  164. state.isCameraReady = true;
  165. break;
  166. }
  167. }
  168. }
  169. }
  170. /// 遍历当前相机列表并启动前置相机
  171. void openFrontCamera() async {
  172. if (_cameras.isEmpty) {
  173. PromptBox.toast('Error: No cameras found.');
  174. } else {
  175. for (CameraDescription cameraDescription in _cameras) {
  176. if (cameraDescription.lensDirection == CameraLensDirection.front) {
  177. await onNewCameraSelected(cameraDescription);
  178. lockCaptureOrientation();
  179. update();
  180. state.isCameraReady = true;
  181. break;
  182. }
  183. }
  184. }
  185. }
  186. /// 相机锁定旋转
  187. Future<void> lockCaptureOrientation() async {
  188. final CameraController? cameraController = kCameraController;
  189. if (cameraController == null || !cameraController.value.isInitialized) {
  190. PromptBox.toast('Error: select a camera first.');
  191. return;
  192. }
  193. if (!cameraController.value.isCaptureOrientationLocked) {
  194. try {
  195. await cameraController
  196. .lockCaptureOrientation(DeviceOrientation.landscapeLeft);
  197. } on CameraException catch (e) {
  198. PromptBox.toast('Error: ${e.code}\n${e.description}');
  199. }
  200. } else {
  201. PromptBox.toast('Rotation lock is already enabled.');
  202. }
  203. }
  204. /// 执行一次拍摄
  205. Future<XFile?> takePicture() async {
  206. final CameraController? cameraController = kCameraController;
  207. if (cameraController == null || !cameraController.value.isInitialized) {
  208. PromptBox.toast('Error: select a camera first.');
  209. return null;
  210. }
  211. if (cameraController.value.isTakingPicture) {
  212. // A capture is already pending, do nothing.
  213. return null;
  214. }
  215. try {
  216. final XFile file = await cameraController.takePicture();
  217. return file;
  218. } on CameraException catch (e) {
  219. PromptBox.toast('Error: ${e.code}\n${e.description}');
  220. return null;
  221. }
  222. }
  223. /// 测试图像文件缓存,print 遍历输出
  224. void debugShowCache() async {
  225. final cacheManager = Get.find<ICacheManager>();
  226. double cacheSize = await cacheManager.getCacheSize();
  227. double imageCacheSize = await cacheManager.getImageCacheSize();
  228. debugPrint('cacheSize = $cacheSize : ${formatSize(cacheSize)}');
  229. debugPrint(
  230. 'imageCacheSize = $imageCacheSize : ${formatSize(imageCacheSize)}');
  231. }
  232. static String formatSize(double value) {
  233. List<String> unitArr = ['B', 'K', 'M', 'G'];
  234. int index = 0;
  235. while (value > 1024) {
  236. index++;
  237. value = value / 1024;
  238. }
  239. String size = value.toStringAsFixed(2);
  240. return size + unitArr[index];
  241. }
  242. /// 保存到相册
  243. void saveImageToGallery(XFile image) async {
  244. // 获取图像的字节数据
  245. Uint8List bytes = await image.readAsBytes();
  246. // 将图像保存到相册
  247. await ImageGallerySaver.saveImage(bytes, quality: 100);
  248. }
  249. /// 处理图像裁切
  250. Future<String> clipLocalImage(XFile soureceImage, double scale) async {
  251. assert(scale >= 1, 'scale must be greater than 1');
  252. // 获取图像的字节数据
  253. Uint8List bytes = await soureceImage.readAsBytes();
  254. var codec = await ui.instantiateImageCodec(bytes);
  255. var nextFrame = await codec.getNextFrame();
  256. var image = nextFrame.image;
  257. Rect src = Rect.fromLTWH(
  258. (scale - 1) / 2 / scale * image.width.toDouble(),
  259. (scale - 1) / 2 / scale * image.height.toDouble(),
  260. image.width.toDouble() / scale,
  261. image.height.toDouble() / scale,
  262. );
  263. Rect dst =
  264. Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble());
  265. ui.Image croppedImage = await getCroppedImage(image, src, dst);
  266. ByteData? newImageBytes =
  267. await croppedImage.toByteData(format: ui.ImageByteFormat.png);
  268. if (newImageBytes == null) {
  269. return '';
  270. }
  271. Uint8List newImageUint8List = newImageBytes.buffer.asUint8List();
  272. /// FIXME 不要存到相册而是存到临时目录
  273. Map<Object?, Object?> result =
  274. await ImageGallerySaver.saveImage(newImageUint8List, quality: 100);
  275. String jsonString = jsonEncode(result);
  276. Map<String, dynamic> json = jsonDecode(jsonString);
  277. String? filePath = json['filePath'];
  278. return filePath ?? '';
  279. }
  280. /// 获取裁切后的图像
  281. Future<ui.Image> getCroppedImage(ui.Image image, Rect src, Rect dst) {
  282. var pictureRecorder = ui.PictureRecorder();
  283. Canvas canvas = Canvas(pictureRecorder);
  284. canvas.drawImageRect(image, src, dst, Paint());
  285. return pictureRecorder.endRecording().toImage(
  286. dst.width.floor(),
  287. dst.height.floor(),
  288. );
  289. }
  290. /// 发生拍摄身份证事件
  291. void onCaptureIdCardButtonPressed() {
  292. takePicture().then((XFile? file) async {
  293. // imageFile = file;
  294. if (file != null) {
  295. await clipLocalImage(file, 1.8);
  296. /// TODO 上传给server,获取返回值信息
  297. if (true) {
  298. PromptBox.toast('身份证识别成功');
  299. idCardInfo.localCardImagePath = file.path;
  300. idCardInfo.idCardName = '金阳';
  301. idCardInfo.idCardGender = '女';
  302. idCardInfo.idCardNation = '汉';
  303. idCardInfo.idCardBirthDate = '1980年10月27日';
  304. idCardInfo.idCardAddress = '北京市西城区复兴门外大街999号院11号楼3单元502室';
  305. idCardInfo.idCardNumber = '110101198010270000';
  306. state.isShowIdCardInfoSwitch = true;
  307. state.isIdCardInfoShow = true;
  308. state.isInFaceRecognition = true;
  309. openFrontCamera();
  310. }
  311. debugShowCache();
  312. }
  313. });
  314. }
  315. /// 发生拍摄人像事件
  316. void onCaptureFaceButtonPressed() {
  317. runDetectionTimer();
  318. // takePicture().then((XFile? file) async {
  319. // // imageFile = file;
  320. // if (file != null) {
  321. // // state.isShowFaceRecognitionResult = false;
  322. // // await doDetection(file.path);
  323. // // state.isShowFaceRecognitionResult = true;
  324. // runDetectionTimer();
  325. // if (true) {
  326. // PromptBox.toast('面部识别成功');
  327. // }
  328. // debugShowCache();
  329. // }
  330. // });
  331. }
  332. /// 发生结束录制视频事件
  333. void onStopButtonPressed() {
  334. stopVideoRecording().then((XFile? file) {
  335. if (file != null) {
  336. PromptBox.toast('Video recorded to ${file.path}');
  337. // videoFile = file;
  338. // _startVideoPlayer();
  339. }
  340. update();
  341. });
  342. }
  343. /// 发生开始录制视频事件
  344. void onVideoRecordButtonPressed() {
  345. startVideoRecording().then((_) {
  346. update();
  347. });
  348. }
  349. /// 暂停录制视频
  350. void onPauseButtonPressed() {
  351. pauseVideoRecording().then((_) {
  352. update();
  353. });
  354. }
  355. /// 恢复视频录制
  356. void onResumeButtonPressed() {
  357. resumeVideoRecording().then((_) {
  358. update();
  359. });
  360. }
  361. /// 开始录制视频
  362. Future<void> startVideoRecording() async {
  363. final CameraController? cameraController = kCameraController;
  364. if (cameraController == null || !cameraController.value.isInitialized) {
  365. PromptBox.toast('Error: select a camera first.');
  366. return;
  367. }
  368. if (cameraController.value.isRecordingVideo) {
  369. // A recording is already started, do nothing.
  370. return;
  371. }
  372. try {
  373. await cameraController.startVideoRecording();
  374. } on CameraException catch (e) {
  375. PromptBox.toast('Error: ${e.code}\n${e.description}');
  376. return;
  377. }
  378. }
  379. /// 停止录制视频
  380. Future<XFile?> stopVideoRecording() async {
  381. final CameraController? cameraController = kCameraController;
  382. if (cameraController == null || !cameraController.value.isRecordingVideo) {
  383. return null;
  384. }
  385. try {
  386. return cameraController.stopVideoRecording();
  387. } on CameraException catch (e) {
  388. PromptBox.toast('Error: ${e.code}\n${e.description}');
  389. return null;
  390. }
  391. }
  392. /// 暂停录制视频
  393. Future<void> pauseVideoRecording() async {
  394. final CameraController? cameraController = kCameraController;
  395. if (cameraController == null || !cameraController.value.isRecordingVideo) {
  396. return;
  397. }
  398. try {
  399. await cameraController.pauseVideoRecording();
  400. } on CameraException catch (e) {
  401. PromptBox.toast('Error: ${e.code}\n${e.description}');
  402. rethrow;
  403. }
  404. }
  405. /// 恢复视频录制
  406. Future<void> resumeVideoRecording() async {
  407. final CameraController? cameraController = kCameraController;
  408. if (cameraController == null || !cameraController.value.isRecordingVideo) {
  409. return;
  410. }
  411. try {
  412. await cameraController.resumeVideoRecording();
  413. } on CameraException catch (e) {
  414. PromptBox.toast('Error: ${e.code}\n${e.description}');
  415. rethrow;
  416. }
  417. }
  418. /// 在 widget 内存中分配后立即调用。
  419. @override
  420. void onInit() async {
  421. // await initCamera();
  422. super.onInit();
  423. }
  424. /// 在 onInit() 之后调用 1 帧。这是进入的理想场所
  425. @override
  426. void onReady() async {
  427. super.onReady();
  428. await initAvailableCameras();
  429. openBackCamera();
  430. }
  431. /// 在 [onDelete] 方法之前调用。
  432. @override
  433. void onClose() {
  434. super.onClose();
  435. final cacheManager = Get.find<ICacheManager>();
  436. cacheManager.clearApplicationImageCache();
  437. closeDetector();
  438. }
  439. /// dispose 释放内存
  440. @override
  441. void dispose() {
  442. super.dispose();
  443. }
  444. @override
  445. void didChangeAppLifecycleState(AppLifecycleState state) {
  446. // FIXME 未执行
  447. super.didChangeAppLifecycleState(state);
  448. print('state = $state');
  449. final CameraController? cameraController = kCameraController;
  450. // App state changed before we got the chance to initialize.
  451. if (cameraController == null || !cameraController.value.isInitialized) {
  452. return;
  453. }
  454. if (state == AppLifecycleState.inactive) {
  455. cameraController.dispose();
  456. } else if (state == AppLifecycleState.resumed) {
  457. onNewCameraSelected(cameraController.description);
  458. }
  459. }
  460. /// WIP
  461. /// 面部识别 基于 Google's ML Kit
  462. ///
  463. Timer? _detectionTimer;
  464. InputImage inputImage = InputImage.fromFilePath('');
  465. FaceDetector faceDetector = FaceDetector(options: FaceDetectorOptions());
  466. Future<void> doDetection(
  467. FaceDetector faceDetector, String imageFilePath) async {
  468. inputImage = InputImage.fromFilePath(imageFilePath);
  469. final List<Face> faces = await faceDetector.processImage(inputImage);
  470. kFrameFacesResult = [];
  471. kFrameFacesResult.addAll(faces);
  472. if (inputImage.metadata != null) {
  473. kFrameImageSize = inputImage.metadata!.size;
  474. } else {
  475. if (kCameraController != null) {
  476. final CameraController cameraController = kCameraController!;
  477. // kFrameImageSize = cameraController.value.previewSize ?? Size.zero;
  478. kFrameImageSize = const Size(1920, 1080);
  479. }
  480. }
  481. for (Face face in faces) {
  482. final Rect boundingBox = face.boundingBox;
  483. final double? rotX =
  484. face.headEulerAngleX; // Head is tilted up and down rotX degrees
  485. final double? rotY =
  486. face.headEulerAngleY; // Head is rotated to the right rotY degrees
  487. final double? rotZ =
  488. face.headEulerAngleZ; // Head is tilted sideways rotZ degrees
  489. // If landmark detection was enabled with FaceDetectorOptions (mouth, ears,
  490. // eyes, cheeks, and nose available):
  491. final FaceLandmark? leftEar = face.landmarks[FaceLandmarkType.leftEar];
  492. if (leftEar != null) {
  493. final Point<int> leftEarPos = leftEar.position;
  494. }
  495. // If classification was enabled with FaceDetectorOptions:
  496. if (face.smilingProbability != null) {
  497. final double? smileProb = face.smilingProbability;
  498. }
  499. // If face tracking was enabled with FaceDetectorOptions:
  500. if (face.trackingId != null) {
  501. final int? id = face.trackingId;
  502. }
  503. }
  504. }
  505. void runDetectionTimer() {
  506. if (_detectionTimer != null) {
  507. _detectionTimer!.cancel();
  508. _detectionTimer = null;
  509. faceDetector.close();
  510. state.isShowFaceRecognitionResult = false;
  511. return;
  512. }
  513. final options = FaceDetectorOptions();
  514. faceDetector =
  515. FaceDetector(options: FaceDetectorOptions(enableContours: true));
  516. state.isShowFaceRecognitionResult = true;
  517. _detectionTimer = Timer.periodic(
  518. const Duration(milliseconds: 200),
  519. (timer) async {
  520. if (kCameraController == null) {
  521. return;
  522. }
  523. final XFile? file = await takePicture();
  524. if (file != null) {
  525. await doDetection(faceDetector, file.path);
  526. update(['face_bounding_box']);
  527. }
  528. },
  529. );
  530. }
  531. /// 销毁检测器
  532. void closeDetector() {
  533. if (_detectionTimer != null) {
  534. state.isShowFaceRecognitionResult = false;
  535. _detectionTimer!.cancel();
  536. _detectionTimer = null;
  537. }
  538. faceDetector.close();
  539. }
  540. }