Flutter中Google MLKit文字识别致低端设备UI卡顿优化求助
优化低端设备上Google MLKit文字识别的UI卡顿问题
我使用google_mlkit_text_recognition和camera API实现图片文字提取,在部分低端设备上出现UI响应异常、卡顿的问题。由于MLKit文字识别必须在主Isolate运行,无法用独立Isolate分担计算;虽已将操作放在async函数中,但高分辨率相机配置下UI仍卡顿。若将分辨率设为.medium,UI恢复正常,但MLKit识别效果明显下降。求优化思路,以下是我的代码:
CameraView 实现代码
import 'dart:async'; import 'dart:io'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:smartdevice/ui/scan_serial_number/serial_number_validator.dart'; class CameraView extends StatefulWidget { Function(String)? onCapture; CameraView({super.key, widget.onCapture}); @override State<CameraView> createState() => _CameraViewState(); } class _CameraViewState extends State<CameraView> with WidgetsBindingObserver { bool _isPermissionGranted = false; final _textRecognizer = TextRecognizer(); bool _isProcessingImage = false; late final Future<void> _future; var result = ''; Timer? _timer; CameraController? _cameraController; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _future = _requestCameraPermission(); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _stopCamera(); _timer?.cancel(); _textRecognizer.close(); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (_cameraController == null || !_cameraController!.value.isInitialized) { return; } if (state == AppLifecycleState.inactive) { _stopCamera(); } else if (state == AppLifecycleState.resumed && _cameraController != null && _cameraController!.value.isInitialized) { _startCamera(); } } @override Widget build(BuildContext context) { final screenSize = MediaQuery.of(context).size; return FutureBuilder( future: _future, builder: (context, snapshot) { return Stack( children: [ if (_isPermissionGranted) FutureBuilder<List<CameraDescription>>( future: availableCameras(), builder: (context, snapshot) { if (snapshot.hasData) { _initCameraController(snapshot.data!); if (_cameraController != null) { return SizedBox( width: screenSize.width, height: screenSize.height, child: CameraPreview(_cameraController!), ); } else { return const CircularProgressIndicator(); } } else { return const CircularProgressIndicator(); } }, ), ], ); }, ); } Future<void> _requestCameraPermission() async { final status = await Permission.camera.request(); _isPermissionGranted = status == PermissionStatus.granted; } void _initCameraController(List<CameraDescription> cameras) { if (_cameraController != null) { return; } CameraDescription? camera; for (var i = 0; i < cameras.length; i++) { final CameraDescription current = cameras[i]; if (current.lensDirection == CameraLensDirection.back) { camera = current; break; } } if (camera != null) { _cameraSelected(camera); } } void _startCamera() { if (_cameraController != null) { _cameraSelected(_cameraController!.description); } } void _stopCamera() { if (_cameraController != null) { _cameraController?.dispose(); } _timer?.cancel(); } Future<void> _listenToCameraStream() async { _timer = Timer.periodic(const Duration(seconds: 1), (timer) async { if (_cameraController != null && !_isProcessingImage) { await _scanImage(); } }); } Future<void> _cameraSelected(CameraDescription camera) async { _cameraController = CameraController( camera, ResolutionPreset.max, enableAudio: false, fps: 30, ); await _cameraController!.initialize(); if (!mounted) { return; } setState(() {}); _listenToCameraStream(); } Future<void> _scanImage() async { if (_cameraController == null) return; _isProcessingImage = true; print('## Processing started.'); try { final pictureFile = await _cameraController!.takePicture(); final file = File(pictureFile.path); final inputImage = InputImage.fromFile(file); final recognizedText = await _textRecognizer.processImage(inputImage); List<String> texts = recognizedText.text.split('\n'); for (final text in texts) { print('## $text'); if (text.isValidSerialNumber) { print('## Found valid serial number $text'); } } } catch (e) { print('## Error taking photos - ${e.toString()}'); } print('## Processing ended.'); _isProcessingImage = false; } }
pubspec.yaml 依赖配置
publish_to: 'none' version: 0.5.0 environment: sdk: 3.5.3 dependencies: bloc: 8.1.4 equatable: 2.0.5 flutter: sdk: flutter flutter_bloc: 8.1.6 go_router: 14.2.0 domain: path: domain data: path: data get_it: 7.7.0 injectable: 2.4.2 cached_network_image: 3.3.1 lottie: 3.1.2 flutter_markdown: 0.7.3 url_launcher: 6.3.0 flutter_localizations: sdk: flutter intl: 0.19.0 youtube_player_iframe: 5.1.3 device_info_plus: 9.1.2 camera: 0.11.0 permission_handler: ^11.3.1 google_mlkit_text_recognition: ^0.13.1 image: ^4.2.0 dev_dependencies: test: 1.25.7 # workaround to have the dependency in the sub packages flutter_test: sdk: flutter flutter_lints: 4.0.0 mocktail: 1.0.4 injectable_generator: 2.6.1 go_router_builder: 2.7.1 build_runner: 2.4.11 melos: 6.1.0 bloc_test: 9.1.7 module: androidX: true androidPackage: com.liebherr.hau.smartdevice iosBundleIdentifier: com.liebherr.hau.smartdevice
优化方案
1. 替换takePicture为实时图像流
takePicture会生成全分辨率物理文件,涉及磁盘IO操作,对低端设备负担极大。改用cameraController.startImageStream直接获取内存中的图像帧,跳过文件写入步骤,大幅减少资源消耗:
void _startImageStream() { _cameraController?.startImageStream((CameraImage image) async { if (_isProcessingImage) return; _isProcessingImage = true; try { final inputImage = await _convertCameraImageToInputImage(image); if (inputImage == null) return; // 预处理图像:缩放至合适尺寸 final processedImage = await _preprocessInputImage(inputImage); final recognizedText = await _textRecognizer.processImage(processedImage); List<String> texts = recognizedText.text.split('\n'); for (final text in texts) { if (text.isValidSerialNumber) { print('## Found valid serial number $text'); _cameraController?.stopImageStream(); widget.onCapture?.call(text); break; } } } catch (e) { print('## Error processing image - ${e.toString()}'); } _isProcessingImage = false; }); } Future<InputImage?> _convertCameraImageToInputImage(CameraImage image) async { final camera = _cameraController!.description; return InputImage.fromCameraImage(image, camera); } Future<InputImage> _preprocessInputImage(InputImage inputImage) async { final decodedImage = await decodeImageFromList(inputImage.bytes); // 缩放到宽度1280,平衡识别精度与性能 final scaledImage = copyResize(decodedImage, width: 1280); final imageBytes = encodeJpg(scaledImage); return InputImage.fromBytes( bytes: imageBytes, metadata: inputImage.metadata, ); }
2. 动态调整相机配置
根据设备性能选择分辨率和帧率,避免低端设备过载:
Future<void> _cameraSelected(CameraDescription camera) async { final deviceInfo = await DeviceInfoPlugin().deviceInfo; late ResolutionPreset resolutionPreset; late int fps; if (deviceInfo is AndroidDeviceInfo) { final ram = deviceInfo.systemMemoryInBytes; // 4GB以下RAM的设备用高分辨率而非最大分辨率,帧率降为15 resolutionPreset = ram < 4 * 1024 * 1024 * 1024 ? ResolutionPreset.high : ResolutionPreset.max; fps = ram < 4 * 1024 * 1024 * 1024 ? 15 : 30; } else if (deviceInfo is IosDeviceInfo) { resolutionPreset = deviceInfo.model.contains('SE') || deviceInfo.model.contains('8') ? ResolutionPreset.high : ResolutionPreset.max; fps = deviceInfo.model.contains('SE') || deviceInfo.model.contains('8') ? 15 : 30; } else { resolutionPreset = ResolutionPreset.high; fps = 15; } _cameraController = CameraController( camera, resolutionPreset, enableAudio: false, fps: fps, ); await _cameraController!.initialize(); if (!mounted) return; setState(() {}); _startImageStream(); }
3. 限制识别频率
避免每帧都处理,设置时间间隔减少计算量:
DateTime _lastProcessingTime = DateTime.now(); void _startImageStream() { _cameraController?.startImageStream((CameraImage image) async { if (_isProcessingImage || DateTime.now().difference(_lastProcessingTime).inMilliseconds < 500) return; _lastProcessingTime = DateTime.now(); _isProcessingImage = true; // 后续处理逻辑... }); }
4. 裁剪感兴趣区域(ROI)
如果仅需识别特定区域(如序列号固定位置),裁剪该区域后再处理,大幅减少计算量:
Future<InputImage> _preprocessInputImage(InputImage inputImage) async { final decodedImage = await decodeImageFromList(inputImage.bytes); // 示例:裁剪屏幕中间60%宽度、20%高度的区域 final roiRect = Rect.fromLTWH( decodedImage.width * 0.2, decodedImage.height * 0.4, decodedImage.width * 0.6, decodedImage.height * 0.2, ); final croppedImage = copyCrop( decodedImage, x: roiRect.left.toInt(), y: roiRect.top.toInt(), width: roiRect.width.toInt(), height: roiRect.height.toInt(), ); final imageBytes = encodeJpg(croppedImage); return InputImage.fromBytes( bytes: imageBytes, metadata: inputImage.metadata, ); }
5. 资源优化
- 在
dispose中彻底停止图像流并释放控制器 - 升级
camera和google_mlkit_text_recognition到最新稳定版,获取官方性能优化 - 关闭不必要的相机功能(如非必需的自动对焦)
内容的提问来源于stack exchange,提问作者Sulaiman Khan
相关产品推荐
相关产品推荐

