controller.dart 18 KB

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