You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 04:32:02