去年接了一个挺典型的项目:某电子元件工厂要给产线加装外观质检环节,检测对象是尺寸只有一两厘米的小零件,主要看划痕、脏污和边缘缺料。客户提了两个很硬的需求——第一,不能依赖服务端,因为车间网络时好时坏,断网停线谁都受不了;第二,操作员把工件放到工位后,两三秒内必须给出判定,否则产线节拍跟不上。最后交付的形态,是一套运行在 HarmonyOS 5.0 平板上的智能工业质检 APP,所有图像识别都通过端侧 AI 在设备本地完成,一条产线一台终端,断网照常干活。这篇就把项目从选型到落地的完整过程拆开讲,覆盖架构设计、关键编码、性能调优和现场踩坑,尤其适合正在做产线质检、设备视觉或边缘智能应用的工程师参考。不管你是用工业相机还是普通 USB 摄像头,只要目标是“端侧实时检测”,这里面的思路基本都能搬走。
1. 为什么工业质检要“把AI放到端侧”——云端方案把我逼出来的选择
1.1 云端质检的三个硬伤
一开始的方案其实很常规:相机拍照,图片传服务器,GPU 推理,结果回传。听上去没什么问题,但真的落到产线环境后,一连串问题全冒出来了。
第一个硬伤是延迟。从按下快门到界面上出现 NG/OK,要经历图片编码、上传、排队推理、结果回传,一个来回普遍在 1 秒以上,赶上服务器负载高直接变 3 秒。产线节拍是按秒算的,工人等不起这种反馈。
第二个是断网。车间里的无线信号穿不过货架和铁皮,时好时坏是常态。质检流程一旦变成“偶尔失效”,项目就没法验收了,因为漏检一次可能就导致整批退货,这个责任谁也担不起。
第三个是数据管控。产线拍的照片涉及工艺参数、模具形貌、产品外观细节,客户明确不希望这些数据跑到外部服务器上。端侧推理天然满足“数据不出设备”,省掉了一堆合规沟通。
这三个硬伤加在一起,让我们把方案整体翻了过来:图像采集、预处理、AI 推理、结果判定全部放到 HarmonyOS 设备本地完成,服务端最多做一个统计看板,不做实时质检。
1.2 HarmonyOS 5.0 给端侧质检提供了什么
经常有人问,为什么非要用 HarmonyOS 5.0,普通平板不行吗?我做完之后最大的感受是,5.0 这套系统在“端侧 AI 开发”上确实省了很多折腾。它把 CPU、NPU 等异构算力统一封装成了系统神经网络运行时,我只需要把模型离线转换成系统支持的格式,代码里负责模型加载、输入输出张量的填充,剩下的算子调度、NPU 加速、性能兜底都交给运行时处理。
这一点对工业质检尤其重要,因为产线设备经常不是同一个品牌、同一个芯片平台。用系统级推理能力,只要设备通过系统兼容认证,跑的就是同一套代码,不需要为每种芯片单独适配推理库,开发和维护成本都降下来了。
UI 层用 ArkTS 和 ArkUI 写起来也顺。实时预览、结果标注、工单记录这类界面,声明式写法改起来快,客户在现场提出“这里加个按钮、那里改个颜色”的需求时,改完基本不会引爆其他页面。
另外,5.0 强调的多设备协同在产线场景也很有用。一个车间里可能有手持终端、平板、工控屏,同一套工程可以适配不同尺寸屏幕。我们后期在几个工位换成了不同品牌、分辨率的设备,核心代码几乎没有大改。
不过话说回来,系统再好也只是地基。真正决定项目能不能落地的,是下面这套流水线设计。
2. 把一条检测流水线拆成五层:从相机硬件的像素到屏幕上的判定结果
2.1 五层链路:每一层做什么,谁接管内存
我习惯把整个质检流程拆成五层:采集层、预处理层、推理层、后处理层、业务层。拆清楚之后,调性能、排查问题都特别方便,因为每一层可以独立优化、独立回滚。
采集层负责把相机的 YUV 帧从硬件 buffer 里拿出来。HarmonyOS 5.0 里建议走 XComponent 结合相机会话,先把帧缓冲区绑定到 surface,再在帧回调里拿 buffer。这一步最忌讳的就是把每一帧都拷贝到 JS 侧,一帧就是几百 KB 甚至几 MB,全拷贝的话,CPU 和内存带宽都扛不住。
预处理层完成三件事:颜色空间转换(YUV 转 RGB)、等比缩放(模型输入 640x640)、归一化(像素值除以 255)。这一步看起来不起眼,但做不好会直接影响识别精度,尤其是颜色顺序和补边方式,后面会专门讲。
推理层就是把预处理好的张量喂给模型,调用系统神经网络运行时,让 NPU 跑算子,拿到原始输出。这里要注意输入张量的内存排布,NCHW 和 NHWC 对小模型推理速度影响不一定明显,但对你手动拼数据的方式影响很大,选错了就会多写一堆转换逻辑。
后处理层做检测框解码、置信度过滤、非极大值抑制,以及我们额外加的连续帧平滑。业务层则负责把结果画到预览面上、灯光提示、蜂鸣器报警、写本地工单记录。
各层之间用环形缓冲区串起来,采集线程和推理线程各拿各的 buffer,prepare、inference、release 用状态机切。这套机制能让每一层不互相阻塞,吞吐量和延迟都变得可预测。
2.2 为什么后处理和业务联动也要放端侧
有人问过,模型推理放端侧可以理解,为什么结果判定也不上服务端?原因是产线场景里,法官其实就是“阈值”和“规则”,逻辑很轻,完全没必要跨网络,更没必要交给服务器。
端侧后处理能及时响应。我们会在 NG 时闪烁红光、触发 IO 信号控制回流线,这个动作需要的是毫秒级响应。如果把结果先发服务器再回来决定动作,光网络乐观估计就够产线停半天。
还有一个隐藏好处:现场调阈值特别方便。客户在现场盯着看,觉得误检多了,直接在设置页把置信度从 0.5 调到 0.62,马上就能生效,完全不需要 IT 介入。这个体验极大缩短了项目验收周期。
2.3 模型选型与离线转换
模型我们用的是轻量目标检测网络,公开的 YOLO 系列剪枝版本就够用。最开始也试过分割网络,精度确实更高,但在端侧实时跑起来的成本明显偏高,而产线检测要的是速度和稳定,目标检测网络的性价比最好。
训练还是老套路:标注缺陷样本,离线训练出 ONNX 模型,再从 ONNX 转成系统神经网络运行时支持的格式。这里有个经验:转换时要指定动态形状为静态形状,也就是把输入尺寸固定下来,转换出来的模型在端侧运行更稳定,也更省内存。
转换完之后别急着集成,先在开发设备上跑一个冒烟测试,拿同一张图对比转换前后的推理结果。这一步能提前发现很多算子兼容问题,比上线后在生产设备上排查舒服得多。
3. 落地代码时需要死磕的三个点:取帧、预处理、推理封装
3.1 相机取帧:别用普通Image组件,直接走XComponent
第一版我用普通 Image 组件打开相机预览,结果 CPU 占用高、帧率低,预览和推理根本没法同时做。后来改成 XComponent 结合相机会话,把预览绑定到 native surface,整个流程才顺起来。
// 伪代码:基于 HarmonyOS 5.0 公开能力编写,主要展示取帧思路 let xComponent = this.getComponentById('cameraXComponent') as XComponent; xComponent.onLoad(() => { let surfaceId = xComponent.getXComponentSurfaceId(); let cameraManager = this.context.getCameraManager(); let cameraInput = cameraManager.createCameraInput( cameraManager.getSupportedCameras()[0] ); let previewOutput = cameraManager.createPreviewOutput(surfaceId); let session = cameraManager.createSession(CameraSessionType.NORMAL); session.beginConfig(); session.addInput(cameraInput); session.addOutput(previewOutput); // 每帧回调里拿到 YUV 数据,直接塞进队列 session.setFrameCallback((frame) => { this.frameQueue.enqueue(frame.getBuffer()); }); session.commitConfig(); session.start(); });这段代码最关键的一点,是帧回调里不干任何耗时操作,只把 buffer 放进队列。相机帧是一个高频事件源,任何一步慢了都会把后面的流水线全部拖垮。
3.2 预处理:对齐模型输入是第一个大坑
模型在训练时输入是什么,端侧预处理就必须严格还原。我们踩的第一个坑是:训练时用的是 RGB 顺序,端侧拿到的原始帧是 NV12,转成 RGB 后忘了确认通道顺序,结果颜色通道错位,检测框偏得离谱。
缩放也要注意。模型输入 640x640,原图比例如果不一致,千万不能直接拉伸,否则物体会形变,漏检率飙升。正确做法是等比缩放后补边(letterbox),补边像素值尽量接近模型训练时背景的平均值,否则会产生一圈肉眼看不到、但模型能感知的干扰。
归一化是另一个容易忽略的点,尤其是浮点与整型的区别。如果推理框架的输入 tensor 是 float32,就要先转成 float 再除以 255;如果是 uint8 直接推理,那就得看模型转换时有没有加上 scale 参数。
还有一点,预处理必须复用内存。每次 new 一个数组,一秒钟 30 帧就是 30 次几 MB 的分配,内存抖动会频繁触发回收,直接用肉眼都能看到卡顿。我习惯提前分配一块固定大小的 byte 数组,反复往里填数据。
3.3 推理封装与结果解析
推理层我封装成了一个检测器对象,核心方法只有三个:setModel、detect、release。setModel 负责加载模型和创建会话,detect 接收预处理好的张量,返回检测结果,release 做资源回收。
输出解析是后处理里最琐碎的部分。检测模型输出通常是一个大数组,包含每个候选框的坐标、置信度和类别。流程是先按类别过滤,再按置信度阈值过滤,最后做非极大值抑制,把重叠的框去重。
有了几十毫秒的推理结果,再经过 NMS、过滤、画框,才能真正变成屏幕上那个红色的缺陷标记。这里我随手加了一个耗时统计,每个阶段都打点,后面性能调优全靠这份数据说话。
3.4 业务联动:从判定到声光电
检测结果出来之后,最重要的不是画框,而是触发业务动作。我们用了简单的状态机:正常、可疑、NG。可疑状态会触发界面提示,连续 N 帧 NG 才判定为最终 NG,然后弹窗、亮灯、蜂鸣,并把当前帧和检测结果写入本地数据库。
本地记录别用文本文件,最好用结构化存储。产线一天大几千条记录,后面要按批次、时间、缺陷类型查询,结构化存储效率高得多,也方便后面导出报表。
4. 实时性从“能跑”到“能上岗”:量化、流水线、跳帧三板斧
4.1 模型量化:INT8带来的收益与回退
端侧 AI 加速最立竿见影的就是模型量化。FP32 模型转成 INT8 后,体积直接缩小到四分之一,推理速度在 NPU 上通常能快三到五倍。大部分缺陷检测任务对数值精度没那么敏感,量化后精度回退通常能控制在 1% 以内。
但并不是所有模型都适合直接量化。有些模型的卷积层、激活层对数值范围很敏感,量化后输出分布会漂移。我们遇到过的情况是:整体精度没怎么掉,但细小划痕的置信度普遍降了 0.1 到 0.2,这类小目标恰恰是产线最怕漏的。
稳妥做法是量化后拿着当天的真实缺陷样本跑一遍回归测试,而不是只看验证集准确率。如果小目标确实掉点,就只量化一部分卷积层,敏感层保留浮点计算,这种混合精度方案在系统推理运行时里也支持。
4.2 采集/推理双线程流水线
单线程串行处理是性能瓶颈的根源。如果采集、预处理、推理依次排队做,每个环节耗时都会叠加,一秒钟只能跑七八帧。改成双线程流水线后,采集线程不断取帧入队,推理线程负责预处理、推理、后处理,两者并行,吞吐量能翻一倍。
队列长度要控制,建议不超过三。队列太长,处理的是几百毫秒前的旧画面,实时反馈滞后;太短又容易丢帧。我们最终采用“队列满则丢弃最新帧”的策略,因为产线场景宁可跳帧也不能滞后,人要的是当下的结果,不是补测过去的结果。
在 HarmonyOS 的并发模型里,CPU 密集型任务建议用 TaskPool 或 Worker,千万别把相机帧回调直接丢到 UI 线程处理,不然你会同时看到预览卡顿和推理掉帧两个问题。
4.3 跳帧与ROI裁剪
如果工件在画面里的位置相对固定,用 ROI 裁剪把背景区域直接去掉,模型需要处理的像素一下就少了一半。前提是夹具足够稳定,不然 ROI 一偏移,缺陷就被裁掉了。
另外,不是每一帧都要全分辨率推理。我们只对关键帧做全分辨率检测,中间帧仅用于预览。连续两帧画面变化很小的,直接沿用上一帧的结果,这样能把有限算力集中在真正重要的位置。
实测下来,这几招合起来能把端侧单帧处理时间从一两百毫秒压到 60 毫秒左右,配合 15 到 20 帧的输入,操作员放上工件后一秒左右就能看到结果。
4.4 一组可以抄作业的实测对照
| 配置项 | 单帧处理耗时 | 备注 |
|---|---|---|
| FP32 全精度,单线程 | 约 180ms | 卡顿明显,没法用 |
| FP32 全精度,双线程流水线 | 约 110ms | 勉强能测试 |
| INT8 量化,双线程流水线 | 约 60ms | 基本满足产线要求 |
| INT8 量化,双线程+ROI+跳帧 | 约 40ms | 稳定运行版 |
这组数据来自我在某条产线上的实际测量,不同设备、模型会有差异,但优化路径是一致的:先量化,再并行,最后裁输入。
5. 误检与漏检的攻坚战:产线要的不是准确率,是稳定
5.1 漏检的元凶:样本、打光、数据增强一个都别省
在生产现场调模型时你会发现,漏检往往不是模型结构的问题,而是“你给模型看的世界”和“模型训练时看的世界”不一致。最典型的坑是打光变化。同一个缺陷,在环形灯下和自然光下拍出来差异巨大,模型在训练集里没见过这种光照,自然就漏了。
我们的做法是先把打光固定下来:环形光源加低角度光,工件放到固定夹具上,保证每一帧画面的亮度波动都很小。这才是真正决定稳定性的东西,模型反而不是最关键的。
样本方面,不要只收集好拍的正样本,缺陷样本尤其小缺陷样本要刻意收集。我建议标注时按缺陷面积分级,把划痕分成“明显、中等、细微”三级,训练时对细微级样本做适当过采样,不然模型天然偏向好检的大缺陷。
数据增强也要贴近真实场景。随机亮度扰动、轻微模糊、小角度旋转这些是模拟产线常见变化,而不是为了刷指标随便用。用得太猛反而会让模型学偏。
5.2 误检过滤:置信度阈值只是最后一道闸
模型给出的原始输出不能直接用。置信度阈值调到 0.5,到生产环境可能还是觉得“报警太多”。我实际用的过滤手段有三件套:最小面积过滤、连续帧投票、类别二次校验。
最小面积过滤很好理解,小于几百个像素的框直接忽略。端侧小目标误检的特征就是框小、置信度高,把过小的框去掉,误检率能降一半。
连续帧投票是更关键的一招:同一位置连续三到五帧都被判定为缺陷,才最终报 NG。孤立的一帧闪烁往往是噪声或反光,真实缺陷在连续帧里都会稳定出现。
连续帧投票的代价是反应延迟。我们设计了两档模式:实时模式(1 帧判定)用于抽检,稳定模式(5 帧投票)用于全检,让用户按产线节拍自己选,不用为固定策略妥协。
5.3 数据闭环:让现场被筛掉的NG帧自动沉淀为训练样本
为了持续降低误检和漏检,强烈建议在 APP 里做样本回传功能:每次人工复核结果时,确认这个框是“真实缺陷”还是“误报”,把标记结果和原图一起存到本地。
工厂网络环境通常不允许设备直接连外网,我们就走局域网共享或者 U 盘导出,定期把新样本拉回训练环境,增量训练后重新出模型,再走模型热更新。这套闭环跑起来之后,模型迭代速度从一个月一版变成一周一版,效果提升非常明显。
标注规范也要固化。我内部规定标签字段至少包含缺陷类型、面积等级、光照条件、批次号,有了这些维度,后期分析漏检原因时才有的放矢,不然一堆图片堆在那里根本没法复盘。
6. 部署之后才是真正麻烦的开始:模型热更新与产线联调
6.1 模型打包与版本管理
模型文件不能随意塞在应用资源目录里,因为每次模型更新都要发整包,太慢了。我们的做法是把模型放在独立目录,配合版本号管理,启动时检查本地版本和配置文件里的期望版本,不一致就触发更新流程。
模型文件建议做哈希校验,防止下载中断或 U 盘拷贝损坏。你会发现生产环境里最普遍的问题不是模型不行,而是文件不完整,这种问题花半个小时才能查出来。
6.2 模型热更新:局域网分发比云端更实用
产线环境走云上更新不现实,我们做的是局域网版本检查:一台管理终端当“更新源”,其余终端启动时访问它的共享目录,比对版本号,拉取新模型文件,拉完校验哈希,再重启推理会话。
热更新时要特别注意:不能直接把正在推理的模型文件删掉,而是先把新模型写入临时文件,下载完成后再做原子替换,替换完重新创建推理会话。这样即使更新到一半断了,也不影响当前生产,下次启动还能继续补。
6.3 产线联调中最容易翻车的三个细节
第一个是不同批次工件的底色差异。同一类工件,上一批是哑光表面,下一批是亮面,预处理均值没跟着变,检测结果就会异常。应对措施是把归一化的均值参数做成可配置项,在设置页里可以微调,不用改代码。
第二个是设备散热。工业平板持续跑 NPU 推理,温度一高就会降频,推理耗时从 60 毫秒直接飙到 120 毫秒以上。我们给平板加了散热背夹,同时在 APP 里做温度监控,超过阈值时自动降帧率保稳定。这个坑如果不提前处理,夏天产线环境会非常明显。
第三个是 USB 相机线材和供电。早期用普通线材,信号干扰一大相机就会掉线,重启后还不能自动恢复。后来换成带屏蔽的短线、独立供电的相机,再补上相机掉线自动重连逻辑,这个问题才彻底根治。产线环境比实验室脏乱差多了,线材和供电这种基础问题一定别侥幸。
最后说点个人体会。这个项目跑下来,我最大的感受是:端侧 AI 在 HarmonyOS 5.0 上做工业质检,技术门槛并没有想象中高,难的是整个系统的稳定性和数据闭环。模型精度做到 97% 很容易,但要把误检率、漏检率、响应时间、设备漂移这些问题全部压到产线可接受的范围,才算真正交付。如果以后有人让我再做一次,我一定会把打光和样本闭环放到比模型架构更优先的位置。这套思路不止适用于质检,凡是“端侧视觉识别+实时反馈”的场景,比如安监、巡检、分拣,都可以直接借鉴。