☰
OpenHarmony+Flutter字符扫描Demo:从相机调用到OCR识别全解析
2026/10/9 5:58:21 网站建设 项目流程

最近在整理一个基于OpenHarmony和Flutter的字符扫描Demo,从工程搭建、原生相机调用到OCR识别,踩了不少坑。这个Demo可以扫描二维码、条形码,也能对图片中的印刷文字做基础识别。文章会把我从架构设计、环境配置到核心代码实现的全过程拆开讲,也会把遇到的问题和解决办法一并交代清楚。如果你正打算在OpenHarmony设备上做Flutter的Camera、图像识别或者OCR相关开发,这篇内容应该能帮你少走一些弯路。

1. 项目背景与技术选型思路

1.1 为什么要在OpenHarmony上用Flutter做字符扫描

OpenHarmony生态的设备越来越多,从开发板到平板、工控机,底层能力已经相对成熟,但应用层的开发范式还在快速演进。很多团队过去积累了大量Flutter业务代码,如果因为换到OpenHarmony就要用ArkTS重写一遍,成本非常高。而Flutter在OpenHarmony上的适配已经能跑通基础渲染和原生交互通道,所以把Flutter业务迁移过来,只做Native层的适配,成了一个很务实的选择。

字符扫描这个场景也很有意思。它不是一个炫技需求,而是大量行业应用里的基础能力,比如库存盘点、扫码登录、快递单拍照录入、票据信息提取。这类功能往往要跑在边缘设备上,需要相机采集、图像处理、字符识别一气呵成。用Flutter做UI层,用OpenHarmony的Native能力做相机采集,再通过通道把数据传给Flutter层展示,既能复用跨平台逻辑,又能贴近系统底层,正好能把两端的优势都用上。

还有一点很关键:OpenHarmony的ArkUI虽然开发体验不错,但遇到需要大量图像处理、频繁刷新的场景,性能优化成本不低。Flutter的渲染管线在自绘引擎下表现稳定,配合Impeller的演进,帧率控制会更可控。而且Flutter生态里已经有配套的相机、图像处理、状态管理方案,能省去不少造轮子的时间。

1.2 技术方案对比:ArkTS原生 vs Flutter跨端

我在动手前先梳理了一轮候选方案,主要就是三条路:纯ArkTS开发、Flutter开发加Native插件、WebView套壳。这里有个基础条件需要注意:OpenHarmony设备上运行Flutter,需要的是OpenHarmony分支的Flutter SDK,不是Google官方的那个全平台版本。从我实测的情况来看,官方对OpenHarmony的适配已经能支撑常规业务开发,但部分插件需要手动处理编译配置。

对比维度ArkTS + ArkUIFlutter + Native插件WebView套壳
开发效率中,需适配多端UI高,一套UI多端复用高
渲染性能依赖系统组件,部分场景卡顿自绘渲染,性能稳定受Web引擎限制
原生能力调用直接调用通过通道桥接受限严重
生态资源相对较少丰富,社区活跃依赖前端生态
字符扫描适配成本需从头写相机和OCR只需写相机模块,OCR可复用难以实现高质量识别

最终我选了Flutter加Native插件的组合。原因很直接:UI层逻辑、扫码历史记录、状态管理这堆东西,Flutter这边已经有成熟的Provider、Riverpod方案,直接拿过来用。相机帧的获取和图像格式转换放到OpenHarmony的Native层,因为这部分涉及CameraKit、图像缓冲区的生命周期管理,用Native写更顺手,也方便以后做性能优化。

1.3 关键技术选型:相机采集、图像预处理、OCR引擎

字符扫描看起来就是拍个照然后识别,但拆开来看,至少要搞定三块内容:相机采集、图像预处理、字符识别引擎。

相机采集我直接用OpenHarmony提供的CameraKit,通过NAPI接口封装成Flutter插件可以调用的方法。需要说明的是,这里不要想着用Flutter插件市场里那套通用camera插件,因为OpenHarmony和Android的Camera接口差别很大,硬套会导致各种编译错误,还不如自己写一个轻量的MethodChannel调用。

图像预处理部分,我引入了OpenCV。OpenCV在OpenHarmony上可以用C++编译成动态库,然后通过NAPI暴露接口。它的作用是把相机采集到的原始格式转换成适合识别的灰度图,做二值化、降噪、边缘检测,为后面的字符识别提供干净的输入。

字符识别引擎的选择是最纠结的一环。开源社区最常见的Tesseract对中英文混合场景支持还不错,但体积偏大。我最后用了Tesseract的C++版本,把它通过NDK交叉编译成OpenHarmony的so库。另一个候选是PaddleOCR的轻量化模型,识别精度更高,但需要处理模型推理框架,对Demo来说太重了。这里我补一句:如果你只做二维码和条形码,其实不需要OCR引擎,用现成的Zxing移植到OpenHarmony就能搞定。我这个Demo做了两层,扫码管扫码,OCR管文本识别。

1.4 工程架构设计

整体架构我分了三层。最上层是Flutter应用,负责相机预览画面的展示、识别结果的渲染和交互逻辑。中间层是通道层,用MethodChannel做Dart和Native之间的通信,里面定义了两个核心通道:一个负责相机预览相关操作,一个负责图像识别相关操作。最底层是OpenHarmony的Native模块,用C++实现,直接调用CameraKit和OCR库。

// Dart侧通道定义示例 class ScanChannel { static const MethodChannel cameraChannel = MethodChannel('com.demo.scan/camera'); static const MethodChannel ocrChannel = MethodChannel('com.demo.scan/ocr'); }

把通信拆成两个通道是为了解耦。相机通道主要管理“开始预览”“停止预览”“获取一帧图像”这类操作,OCR通道只做“识别这张灰度图”“识别这张彩色图”这样相对独立的运算。这样后面如果要把OCR能力抽成独立服务,不用动相机相关的代码。

图像数据在通道之间传递时,我用的是字节数组加格式标记。相机返回一帧NV12或者YUV数据,先经过Native层转成OpenCV的Mat,完成预处理后,再把压缩过的JPEG字节数组传给Dart层用于缩略图展示,而识别结果则直接以JSON字符串传回去。这样既避免了频繁传输大体积原始帧导致的卡顿,也把识别逻辑和UI展示切分干净。

2. 开发环境准备与工程搭建

2.1 工具链清单

构建这个项目需要的环境比普通Flutter项目稍微啰嗦一点,我把清单列一下:

  • DevEco Studio 4.0以上版本,用于OpenHarmony工程的编译和签名。
  • OpenHarmony SDK,在DevEco Studio里配置好SDK路径。
  • Flutter的OpenHarmony分支SDK,建议用release分支,稳定一些。
  • Visual Studio Code或者直接用DevEco内置的编辑器写Flutter层代码。
  • Node.js,因为OpenHarmony的构建脚本会用到。

这里最容易被卡住的是Flutter SDK的选择。Google官方和OpenHarmony分支不是一回事,如果直接拿官方版Flutter去跑OpenHarmony工程,会提示找不到ohos平台。我用的是openharmony-tpc仓库维护的flutter,下载下来以后把bin目录加到PATH,执行flutter doctor时能看到ohos类型的设备支持,那才算配置对了。

2.2 创建Flutter项目并适配OpenHarmony

我建议先创建Flutter工程,再去适配OpenHarmony,因为Flutter侧的文件结构比较固定,后面加Native模块不会影响现有代码。命令也很简单:

flutter create -t app --platforms=android,ohos scan_demo

需要注意,--platforms参数里要包含ohos,这是OpenHarmony适配分支特有的参数。如果创建时没有这个选项,说明你的Flutter SDK不是OpenHarmony分支。

创建完成后,工程里会多出一个ohos目录,里面是OpenHarmony的工程骨架。打开ohos/entry/src/main/module.json5,检查一下abilities配置,确认入口Ability指向Flutter的运行入口。这部分容易漏,我一开始跑起来的时候白屏,就是因为入口没有指向生成Flutter页面的Ability。

2.3 配置相机权限和依赖

字符扫描肯定要访问相机,所以权限配置必须提前搞定。OpenHarmony的权限声明和Android类似,要在module.json5里申请,同时还需要在代码里动态向用户发起授权请求。

{ "module": { "requestPermissions": [ { "name": "ohos.permission.CAMERA" } ] } }

动态授权我写在Native层。Flutter层无法直接弹出系统授权框,必须在Ability的onCreate里调用requestPermissionsFromUser,拿到结果后再通过事件回调通知Flutter层。很多新手在这个环节会卡住:明明配置了权限,预览还是黑屏,十有八九是动态授权没有申请成功,或者是在后台线程里调了授权接口,导致UI线程没有响应。

依赖方面,Flutter侧我用了Provider做状态管理,还引入了image_picker做相册选择兜底。Native侧主要是OpenCV和Tesseract的预编译库,放在thirdparty目录,并在CMakeLists里配置链接路径。这些库的体积比较大,如果用4G内存的开发板跑,需要注意加载顺序,避免同时加载多个大库导致内存溢出。

3. 核心实现:相机调用与字符识别

3.1 设计统一的字符扫描接口

编码之前,我先想清楚了这个功能对外暴露的形态。理想情况下,调用方不需要关心底层是扫码还是OCR,只需要传入一个图像源,拿到一个解析结果。所以我在Dart层定义了一个抽象类:

abstract class CharacterScanner { Future<ScanResult> scanImage({required Uint8List imageBytes, ScanType type = ScanType.mixed}); Stream<ScanResult> startContinuousScan(); Future<void> stopContinuousScan(); }

ScanResult统一包含识别类型(二维码、条形码、文本)、原始字符串、识别耗时这些字段。这种设计的好处是,后期无论底层换成PaddleOCR还是其他模型,只要保持这几个方法签名不变,业务侧完全不用动。

连续扫描用Stream流式返回,这样在扫码场景下,相机预览画面里每一帧都会触发识别,一旦命中有效结果就停止并返回。效率上不需要每帧都跑全流程,可以用一个简单的帧间隔控制:默认每300毫秒处理一帧,既不会漏扫,也不会让设备发烫。

3.2 通过MethodChannel调用OpenHarmony原生能力

Dart和原生层的通道注册是分两步的。Dart侧在main函数里设置好通道的处理逻辑,原生侧在Module初始化时注册对应的实现。

class ScanNativeBridge { static const MethodChannel _channel = MethodChannel('com.demo.scan/native'); static Future<void> initialize() async { _channel.setMethodCallHandler((call) async { switch (call.method) { case 'onPermissionGranted': // 处理授权结果 break; case 'onFrameReady': // 收到原生层回调的帧数据 break; } }); } }

原生侧主要用NAPI接口来导出函数。因为OpenHarmony的Native层开发支持C++,我写了一个napi_module_register的注册入口,把startCamera、stopCamera、recognizeImage这几个方法暴露出去。在C++函数内部,通过napi_get_cb_info取出Dart层传来的参数,再把结果通过napi_create_uint8_array或者napi_create_string_utf8封装返回。

这里要特别注意数据生命周期的管理。相机回调拿到的帧是内存池里的缓冲区,一旦回调返回,缓冲可能被系统回收。所以在C++层拿到帧数据后,要立刻拷贝到自建的Mat对象里,不能在回调函数外部继续引用原地址。这个坑我踩了一次,最开始直接把指针传到OpenCV的构造里,画面运行几分钟后就会出现随机性崩溃,就是缓冲区被复用导致的。

3.3 相机预览与帧捕获实现

OpenHarmony的CameraKit提供了比较完整的相机控制接口。核心逻辑是先获取CameraManager,然后枚举相机设备,挑选后置摄像头,创建输入和输出,最后配置会话。

预览画面我直接在Native层用Surface来承接,但Flutter层需要显示预览,就需要把Surface的纹理ID传给Flutter侧,用Texture组件来渲染。另一种折中方案是走ImageReader通道,把每一帧的YUV图像取出来,转换成RGB格式,再用Flutter的Image控件频繁刷新。前面那种方案性能好,但实现复杂;后者实现简单,适合Demo。

我的Demo用了折中做法:正常预览走Texture方案,连续扫描时每300毫秒从ImageReader里拿一帧图像,做识别。这样既让用户看到流畅的预览,又不会把整个预览通路变成识别通路。

// 伪代码:帧回调中触发识别任务 void OnImageAvailable(AVImageReader* reader) { // 获取最新帧 OH_Image *image = OH_ImageReader_ReadNextImage(reader); // 转换成Mat Mat frame = ConvertImageToMat(image); // 灰度化 cvtColor(frame, gray, COLOR_RGBA2GRAY); // 回调到Dart层 SendFrameToDart(gray); OH_Image_Release(image); }

识别过程放在Native层的一个后台线程里执行,这样不会阻塞相机回调。由于OCR耗时比较长,我加了任务队列,避免新帧覆盖正在处理的旧帧导致结果乱序。

3.4 图像预处理与字符识别流程

字符识别不是直接把彩色图丢给Tesseract就完事,那样识别率会低得离谱。我总结了一套比较稳定的流程:

第一步,灰度化。把RGBA或NV12数据转成单通道灰度图。 第二步,降噪。用高斯滤波把相机传感器带来的噪声抹掉,特别是弱光环境下,这一步很关键。 第三步,二值化。我用了自适应阈值,因为环境光照不均匀,固定阈值的效果很差。 第四步,形态学处理。用开运算把一些孤立的噪点去掉,闭合运算把字符笔画里的断裂连接起来。

Mat ProcessImage(const Mat& input) { Mat gray, denoise, binary; cvtColor(input, gray, COLOR_BGR2GRAY); GaussianBlur(gray, denoise, Size(3, 3), 0); adaptiveThreshold(denoise, binary, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY, 31, 15); Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3)); morphologyEx(binary, binary, MORPH_OPEN, kernel); morphologyEx(binary, binary, MORPH_CLOSE, kernel); return binary; }

预处理完的图像喂给Tesseract,我配置了中英文混合的字符白名单。对于纯数字条码,白名单是0123456789,识别速度更快,准确率也更高。对于混合文本,还需要按行分割,做轮廓分析来过滤掉非文字的干扰块。

OCR引擎初始化的过程比较耗时,我把它设计成单例,在App启动后的后台线程里提前加载语言包和模型,而不是等到用户第一次扫码才初始化。这样用户体验会好很多,实测下来冷启动时间从1.8秒降到了0.4秒左右。

3.5 结果展示与交互优化

识别结果我设计成底部的卡片形式。识别成功时,卡片弹出来,展示原始字符和识别时间,同时震动提醒。失败时,只把当前帧的缩略图下载到本地,方便之后调试。

交互上有一个细节:连续扫码时如果扫到了同一个内容,要设置去重逻辑。我在结果里加了一个识别字符串的hash,10秒内重复出现相同内容就直接忽略,避免在同一个画面里反复触发。去重逻辑写在Dart层,用Provider管理扫描历史的集合,每次拿到新结果先查一下集合,再决定是否弹窗。

Flutter的UI渲染这里,我用了一个自绘的识别框overlay。识别框的坐标是从图像上的字符区域映射来的,映射在Dart层完成:先根据原始帧图像尺寸和预览Texture的显示尺寸计算缩放比例,再把识别出的字符包围盒坐标换算到屏幕坐标系。这个换算如果比例不对,会导致识别框完全偏离字符位置,排查起来很费劲。

4. 踩坑实录与性能优化

4.1 常见问题速查表

把开发过程中最典型的几个问题整理成表,方便快速对照。

问题现象可能原因解决办法
Flutter工程创建时找不到ohos平台使用的SDK不是OpenHarmony分支换用OpenHarmony适配的Flutter SDK
相机权限已配置仍然黑屏动态授权没在Native层发起在Ability的onCreate里调用requestPermissionsFromUser
Native库加载崩溃so库未与目标CPU架构匹配用正确工具链重新编译,或确认abiFilter
MethodChannel调用返回null原生方法未注册或注册时机过晚检查napi_module的注册函数,确认在Flutter引擎启动前调用
OCR识别结果乱码图像预处理不到位或没有配置语言包增加自适应二值化,确保tesseract训练数据完整
预览画面卡顿帧回调里做了图像解码把耗时操作移到后台线程,使用ImageReader代替直接回调
连续扫码重复弹窗缺少去重逻辑在Dart层维护一段时间的扫到结果缓存

这些坑里,最值得说的还是Native库的架构匹配。OpenHarmony的NAPI模块编译时,需要针对目标设备的ABI选择工具链。我一开始直接用默认配置编,结果在RK3568的开发板上跑,直接报dlopen failed,后来手动指定了arm64-v8a重新编才解决。

4.2 Flutter组件通信与状态管理实践

刚开始写这个Demo时,我用了最简单的setState来管理相机状态,结果预览页、结果页、历史记录页之间要同步大量状态,代码很快就失控了。后来我换成了Provider,将相机控制、识别结果、权限状态拆成三个ChangeNotifier,通过Provider管理依赖关系。

class ScanState extends ChangeNotifier { ScanStatus _status = ScanStatus.idle; ScanResult? _lastResult; List<ScanResult> _history = []; void onResultReceived(ScanResult result) { _lastResult = result; _history.insert(0, result); notifyListeners(); } }

组件之间的通信主要有两条路径。按钮触发开始扫码,通过context.read<ScanState>().start()去调用Native层;相机帧数据从Native流回来,则通过通道回调解析后,更新ScanState里的数据。这里要注意Flutter的异步事件不会自动触发UI更新,必须确保回调跑在Platform线程,并在调用notifyListeners时使用WidgetsBinding.instance.addPostFrameCallback来避免UI刷新时序问题。

Provider还有一个好处是页面销毁时能统一清理资源。我在dispose方法里调用了ScanNativeBridge.stopContinuousScan(),保证用户退出预览页时相机不会一直开着,功耗问题和摄像头占用问题都一起解决了。

4.3 渲染性能与Impeller引擎

Flutter在新版本里默认开始启用Impeller渲染引擎。OpenHarmony的适配分支也已经支持Impeller,只是默认可能关着。我实测了一下,开启Impeller后,识结果卡片的动画和相机预览叠加层的刷新明显更平滑。原因是Impeller在统一渲染模式下,Shader的准备阶段不再卡顿,避免了旧Skia引擎在复杂场景下的“跳帧”。

不过Impeller在低端设备上不一定完全是好事。如果用比较老的GPU,可能导致首次加载时间变长。我建议通过渐变发现的方式,在设置里保留一个开关,让用户在性能模式和数据模式之间切换。

// 通过配置启用Impeller flutter run --enable-impeller

开启后,字符串高亮、裁剪动画这类多层绘制的帧时间从10毫秒左右降到了6毫秒,对扫描这类需要快速响应的场景帮助很明显。当然了,如果你使用自定义着色器比较频繁,还需要在调试阶段多做几轮压力测试,避免新引擎带来视觉差异。

4.4 真机调试技巧

OpenHarmony设备调试和Android类似,可以通过USB连接后使用DevEco Studio的HiLog查看日志。我推荐三个调试习惯:

第一,千万别只用print打日志。用hilog配合日志级别过滤,才能在海量输出里找到关键信息。比如相机报错会输出在Camera标签,OCR报错会输出在Ocr标签。

hilog -t Camera -e "error"

第二,Flutter的hot reload在OpenHarmony上基本能用,但在修改Native代码后不会生效,需要重新编译安装。所以我的流程是,改Dart代码用hot reload,改C++代码就停止整个App再重新跑。

第三,设备有限的场景下多用模拟器,但要注意OpenHarmony模拟器对相机支持有限,最终必须适配真机。我一般会准备两台设备,一台高配平板做性能验证,一台低配开发板做兼容性验证,这样能更快暴露性能和内存问题。

5. 未来扩展与个人经验

5.1 从Demo到产品:还需要做什么

目前这个Demo还是一个偏“可运行”的阶段,距离产品化还有一些工作要做。

首先是识别能力升级。Tesseract对清晰印刷体表现尚可,但遇到手写体、复杂背景或者歪斜的字,效果会明显下降。后续可以考虑接PaddleOCR的端侧模型,识别率会高一个档次,代价是模型体积和算力需求都上升。另一种思路是使用提供云识别API的第三服务,但这样就把网络依赖引入进来了,适合对隐私不敏感的场景。

其次是扫描逻辑工程化。现在的帧率和识别任务队列是固定参数,实际场景中人眼对预览流畅度的要求是可变的。后续可以做一个动态帧率调节,当识别到画面模糊时自动降低识别频率,保持预览优先;当画面稳定时提高识别频率,缩短触发结果的时间。

还有一个细节很值得处理:多语言支持。我的Demo目前只做中英文混排,但如果放到国际化产品里,还要考虑阿拉伯语、日文等文字特性。处理这些语言时,字符切分和白名单策略都需要专门调整,这一点在架构上要预留好扩展位。

5.2 我对这套技术栈的真实感受

把整个Demo做完之后,我对OpenHarmony和Flutter结合的判断更加清晰:这套组合用于业务界面复杂、需要跨端复用的产品很合适,但在底层相机、图像处理这种路径上,还是得老老实实写Native代码。如果项目对摄像头有极高的帧率和低延迟要求,比如说高速扫码枪那种场景,我建议直接走纯ArkTS加C++的NDK方案,不要绕一圈Flutter,毕竟通道通信和UI线程切换的损耗摆在那里。

反过来,如果项目重UI交互、重业务状态、需要快速迭代,那么Flutter在OpenHarmony上的优势会非常明显。尤其是社区里的状态管理、路由、动画方案都能平滑复用,团队转型成本很低。我个人接下来的计划是把扫描能力再打磨一下,尝试接入更好的OCR模型,并且把通道层的数据传输从字节数组改成共享内存,看能不能把识别吞吐量再提上去。踩过这次项目的坑之后,我再做同类功能心里就有谱了,也希望分享的内容能给你们节省一些时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询