最近我在折腾安卓端的实例分割,最终落地的方案是 YOLOv8 nano 版分割模型,转换后整体大小压在 4.8MB 左右,手机上跑起来能稳定接受,画面还有实时蒙版叠加。这个组合想解决的核心问题很直白:边缘设备算力有限、内存敏感,但又要拿到“看得见轮廓”的分割效果,而不是只给个矩形框。我先把结论放在前面:YOLOv8n-seg 转 NCNN fp16 格式,再配上合理的后处理管线,中端安卓机基本能做到 20~30FPS,工程上完全具备产品化能力。这篇文章我会从模型选型、转换部署、安卓端工程集成,到后处理优化和踩坑排查,把整套链路完整讲一遍,适合正在做边缘AI落地、或者想在安卓上跑分割模型的朋友直接参考。
1. 为什么最终选了 YOLOv8n-seg:4.8MB 模型也能做精细分割
先说说选型的逻辑。当时项目需求是“在安卓设备上识别人、车、常见物体,并且输出物体轮廓”,意味着不能只做检测框,得把 mask 也画出来。传统语义分割模型(比如 DeepLabV3、UNet)在桌面端表现不错,但到了手机端,模型体积动辄几十上百MB,还得考虑 GPU 加速和内存占用,基本劝退。后来把目光放到 YOLOv8 系列,其实原因很简单:它同时支持检测、分割、姿态、分类四个任务,尤其分割任务专门设计了轻量的分割头,和常规实例分割模型比,参数量少很多,适合边缘端。
这里还要强调一下 nano 这个版本的意义。YOLOv8 按规模分 n/s/m/l/x,n 是 nano,也就是参数最少的版本,参数量大约 3.2M 左右。单看这个数字可能没概念,我用实际转换结果说明:FP32 的 ONNX 模型大约 7.1MB,转成 NCNN 并开启 FP16 存储后,param 加 bin 的两个文件合计在 4.8MB 上下。如果继续做 INT8 量化,还能再压到 2MB 以内,但精度会损失一些。对于一个安卓 App 来说,模型包能控制在 5MB 以内,是一件非常舒服的事情——不用考虑动态下载模型、不用纠结 APK 体积超标,甚至可以直接打包进 assets 目录。
另一个选型理由是部署生态。YOLOv8 官方训练、导出工具的链路非常成熟,从 PyTorch 权重到 ONNX,再到 NCNN / TFLite / OpenVINO,每一步都有现成脚本。相比自己从头训练一个分割模型,YOLOv8 的预训练权重在 COCO 上有很好的泛化能力,如果后面要针对特定场景做增量训练,也只是在预训练基础上微调,成本低很多。我在实际项目里就是这么干的:先用 COCO 预训练权重跑通整个安卓链路,后面业务需要识别特殊物体时,再用自己的数据集微调,替换权重即可,其他代码完全不用动。
当然,4.8MB 模型也不能指望它什么都干。实测下来,YOLOv8n-seg 对中等大小物体、轮廓清晰的目标(人、汽车、猫狗这类)分割效果相当不错,但对极小目标或密集重叠场景,mask 边缘会出现锯齿或粘连。所以从业务角度要提前明确预期:边缘端部署追求的是“可用”,不是“极致”。如果你要的是医疗影像那种像素级精确分割,那还得上大模型,但代价是手机端跑不动。我始终觉得,边缘AI 项目里最重要的决定不是谁家模型精度高,而是算力、体积、速度、精度四个维度里,先想清楚哪个能妥协、哪个不能妥协。
1.1 边缘端分割模型赛道:为什么没选其他方案
我简单对比过几类主流方案,方便新入坑的朋友理解选型思路。第一类是经典语义分割模型,比如 DeepLabV3+/UNet,优势是边界质量高,劣势是模型重、推理慢,而且很多实现没有针对移动端做算子优化,跑到安卓上兼容性还容易出问题。第二类是检测模型的延伸,比如 YOLOv5-seg、YOLOv8-seg,这就是我最终走的路线,优点是检测分割一体、单模型输出多任务、部署库成熟,缺点是边界精细度不如专精分割模型。第三类是移动端专用模型,比如 MobileNet 系列做骨干网络的 DeepLabV3 变体,这类在国内很多文章里会推,但实际工程量不小,数据标注、训练流程都要自己搭。
说实话,除非你有非常强的定制化需求,否则在 2024 年还不推荐从零训练分割模型。YOLOv8 的官方仓库已经把数据标注、训练、验证、导出这条链路全部打通了,社区资料也够多,遇到问题搜一下基本都能解决。一个边缘AI 项目的核心矛盾是“能不能按时上线”,而不是“我有没有写出一个原创网络结构”。当然,如果你们团队本身就是做算法研究的,那另说——但那种场景通常也不会把模型直接部署到几十块钱的安卓盒子上。
1.2 nano 版本在算力与精度上的平衡点
有人可能会问,nano 参数这么少,精度够用吗?实测数据可以参考:YOLOv8n-seg 在 COCO 验证集上的 mask mAP 大概在 31 分左右,对比 s 版本大概 36 分,m 版本 43 分。如果单纯看这个数字,确实掉了几分,但你要意识到,mAP 是跨所有类别的平均,落在具体业务里,可能只是一些难例样本的边界变差了,人、车这些常见目标的 mask 依旧可用。
再从推理速度角度看。我用同一台骁龙 778G 手机测试,输入尺寸 512×512,FP16 NCNN + Vulkan 加速,YOLOv8n-seg 单帧推理大约 45~55ms,分割头合成 mask 加后处理再算 15~20ms,整条链路能压在 70ms 左右,换算下来接近 15FPS。如果输入尺寸降到 320×320,推理时间能再缩短到 25ms 上下,总耗时 50ms 以内,20FPS 很稳。这里还没做线程级优化,如果合理利用双线程或 GPU 后端,还有提升空间。对比 m 版本,哪怕用上 Vulkan 也很难到 15FPS,更别说内存占用可能直接翻倍。所以结论很明确:追求安卓端实时,nano 是最合适的分割模型量级。
2. 模型准备与转换:从 YOLOv8n-seg 到 4.8MB 的完整流程
确定了用 YOLOv8n-seg,接下来的工作就是把模型从 PyTorch 的 .pt 权重转成安卓端能用的格式。我先说结论,最终我选择的是 NCNN 框架,因为它在移动端 CPU 和 GPU 上都有比较均衡的表现,集成体积也小。这条链路具体是:YOLOv8n-seg.pt → ONNX → 简化图 → NCNN param/bin。
第一步,安装 ultralytics 环境并导出 ONNX。以 COCO 预训练权重为例,最简单的方式是命令行直接执行:
yolo export model=yolov8n-seg.pt format=onnx opset=12这里有几个关键参数要注意。opset建议用 12,因为 NCNN 对高版本 opset 的支持不一定跟得上,有些算子会转换失败。imgsz默认是 640,这个尺寸对应后续安卓端输入,如果打算用 512 输入,可以导出时就指定imgsz=512,这样模型内部结构更干净。如果要进一步缩小体积,可以加half=True导出 FP16 的 ONNX,但我实际测试发现,直接在 NCNN 阶段做 FP16 转换更稳妥,ONNX 阶段半精度有时会引入奇怪的算子。
导出完成后,先别急转 NCNN。Ultralytics 导出的 ONNX 里通常带有较多冗余结构,直接转 NCNN 也能转,但会多出不少无效算子和性能损耗。我一般先用onnx-simplifier简化一遍:
python -m onnxsim yolov8n-seg.onnx yolov8n-seg-sim.onnx这一步会把常量折叠、算子融合处理掉,输出张量名也更规范。简化完以后,再用onnx2ncnn工具转换:
onnx2ncnn yolov8n-seg-sim.onnx yolov8n_seg.param yolov8n_seg.bin转换成功后会生成两个文件:param 是网络结构描述,bin 是权重参数。此时如果直接拿去用,bin 还是 FP32,体积大概 7MB。要压到 4.8MB,需要用ncnnoptimize做 FP16 半精度优化,同时还能消除一些转换过程中产生的冗余算子:
ncnnoptimize yolov8n_seg.param yolov8n_seg.bin yolov8n_seg_opt.param yolov8n_seg_opt.bin 65536命令最后的65536是量化标志,对应 FP16 存储;0表示 FP32;另外还有65536 | 1之类组合,不过我建议直接用 65536。优化完之后,param 和 bin 加起来一共 4.8MB 左右,这就是标题里那个数字的来源。
如果你对模型体积有更极致的需求,可以继续做 INT8 量化,NCNN 提供了ncnn2int8工具,但需要准备校准数据集。据我测试,nano 模型 INT8 量化后 mask 边界会明显变粗糙,建议非必要不量化,FP16 对精度的影响基本可忽略,体积已经能减半,性价比更高。
2.1 导出转换时绕不开的坑
我在这条链路上踩过好几个坑,挑典型的说说。第一个坑是输出节点不一致。不同版本 ultralytics 导出的 ONNX,输出名可能是output0、output1,也可能直接叫out0、out1。我建议转换完先用一个小脚本打印 ONNX 图的输入输出节点,确认一下名称和维度,不要靠猜。我自己用 Netron 可视化工具看输出节点,效率也很高。
第二个坑是 batch 维度。导出 ONNX 时默认 batch=1,这没问题,但有些操作如果设置成dynamic导出,NCNN 转换会失败或者推理时输入尺寸受限。建议固定 batch=1,后续推理时如果需要多路输入,就多创建几个 extractor 实例,别在模型动态维度上找麻烦。
第三个坑是Detect层的特殊结构。YOLOv8 的检测头在推理时包含了 sigmoid 等激活函数,导出到 ONNX 后会变成普通算子,NCNN 转换时通常没问题。但如果你的模型是自定义改过的检测头,比如加了注意力机制或新的解耦头,就要小心,可能某些自定义算子(例如GridSample、CumSum)NCNN 不支持,需要手动实现或替换。建议在转换前先用 Netron 看一眼图,凡是出现 NCNN 不支持的算子,就尽早换个实现方案,别等安卓端报了layer not found再回头改。
2.2 如何确认转换后的模型没变“傻”
模型转换完不能直接丢给安卓端就完事,一定要先在 PC 端做一次精度验证。我常用的办法是拿一张 COCO 验证集的图片,先跑原版 PyTorch 模型,得到检测框、类别、mask;再用 NCNN 的 Python 版或者 C++ 版推理同一张图,对比两者结果。具体做法是安装ncnn的 Python 包,写一个简单的推理脚本加载 param/bin,输入同一张图,输出检测结果再叠加 mask。只要类别一致、mask 大致重合,说明转换没有大问题。
这一步最好不要省。我遇到过一种情况:ONNX 简化时把某个 concat 的轴搞错了,导致模型输出完全乱序,PyTorch 检测结果正常,NCNN 推理后所有框都在左上角堆成一团。如果当时没做对比验证,直接集成到安卓端,排查起来会非常痛苦。
3. 安卓端实时推理链路:从摄像头画面到透明蒙版
模型准备好了,接下来是最有工程含量的部分:安卓端实时推理。这里面的核心不只是“调用模型”,而是要设计一条从摄像头帧到渲染画面的低延迟管线。我最终的实现分为四步:摄像头采集、图像预处理、NCNN 推理、后处理与渲染。每一步都有各自的坑,分开讲。
先交代一下推理框架。开发安卓端时,我在 NCNN、ONNX Runtime Mobile、TensorFlow Lite 之间做了对比。TFLite 对 Android 的支持最成熟,但分割类模型的算子兼容性一般,很多 YOLOv8-seg 导出后的结构需要手工调,麻烦。ONNX Runtime Mobile 集成简单,但体积增加明显,而且 GPU 支持需要找对应的 EP,坑也不少。NCNN 在我实际的测试中,体积最小、FP16 支持最好、Vulkan GPU 加速也比较省心,所以最终选了它。NCNN 的接入方式是通过 JNI 在 C++ 层调用,Android 端通过System.loadLibrary加载 so 库,然后调用 native 方法。
3.1 摄像头采集与图像预处理:别让 RGB 转换拖慢速度
安卓摄像头常用的方案有 CameraX 和 Camera2。CameraX 封装层级高,代码简单,适合快速原型;Camera2 控制力强,适合做高帧率采集。我这里用的是 CameraX + ImageAnalysis,把ImageProxy的YUV_420_888格式帧转成 RGBA,再传给 JNI。
很多开发者会忽略一个问题:YUV 转 RGB 本身就有不小的 CPU 开销。如果每帧都用一个低效循环转换,400 万像素的图像轻松吃掉十几毫秒。推荐用libyuv或 Android 自带的RenderScript(已经废弃)做转换。我在实践中最终直接用了 NCNN 自带的像素转换逻辑,配合pixel_format=PIXEL_RGBA2RGB和from_pixels_resize,一次调用完成从 RGBA 到 RGB 并缩放到模型输入尺寸的操作,省掉了中间拷贝。
预处理这一层还有个细节:模型的归一化方式。YOLOv8 默认是像素值除以 255,然后用 0 均值、0 方差系数,这么写其实是“不做均值归一化、只做缩放到 0~1”。NCNN 的substract_mean_normalize接口可以接收两个数组,我传的是:
const float mean_vals[3] = {0.f, 0.f, 0.f}; const float norm_vals[3] = {1/255.f, 1/255.f, 1/255.f};千万别把 mean 填成 ImageNet 那套{0.485, 0.456, 0.406},那是给分类用的,YOLOv8 检测头训练时用的归一化方式不同,填错了整个模型输出都是乱的,表现是置信度极低、什么都检测不出来。
3.2 推理线程与内存复用:避免 GC 导致帧率抖动
实时推理中最容易犯的错误是“哪里要数据,哪里就 new 一个对象”。Android 的内存回收机制有时候会让 GC 卡顿找上门,帧率从 25 一下掉到 10,非常影响体验。我的做法是:在 native 层预先分配好足够大的内存缓冲,反复使用,不在循环内部频繁 new/free。
NCNN 的Extractor对象也不是线程安全的,如果每帧都create_extractor,会有初始化开销。我建议把ncnn::Net和Extractor都作为长期存活的对象管理。Net 加载一次模型后直接复用,Extractor 可以每次创建,因为它的成本已经很低。如果有多路输入或需要并行推理,就创建多个 Net 实例,或者给每个线程一个独立的 Net 副本,避免锁冲突。
摄像头回调、推理线程、UI 渲染三者之间也需要合理分工。我采用的生产者-消费者模型是:Camera 回调线程只做 YUV 转 RGBA,然后把结果丢进一个有界队列;推理线程等待队列数据,执行预处理和模型推理;推理完成后把结果传给主线程,用Bitmap或SurfaceView渲染。这样即便某帧推理超时,相机侧也不会阻塞,后一个回调可以继续处理,最多丢帧,不会卡死。
3.3 分割头后处理:从两个输出张量到画面上的蒙版
这是整个工程里最容易写错、也最影响效果的地方。YOLOv8n-seg 在推理时会有两个输出:一个是检测头输出,包含检测框、类别、mask 系数;另一个是 prototype mask,也叫原型掩码。具体到 COCO 模型,检测头输出维度是 (1, 116, 8400) 或者类似格式,其中 4 维是框坐标,80 维是类别得分,32 维是 mask 系数;prototype mask 是 (1, 32, 160, 160),对应输入尺寸 640 的 1/4 分辨率。
mask 的合成方式是:对某个检测到的目标,取它的 32 个 mask 系数,和 prototype mask 做加权求和,再套一个 sigmoid,得到这个目标的概率掩码,最后按阈值二值化并缩放到原图尺寸。公式描述就是:
mask = sigmoid( sum_i(coef_i * proto_i) )后处理的代码逻辑建议这样组织:
// det_out 解析 for (int i = 0; i < numProposal; i++) { float* ptr = (float*)det_out.data + i * stride; // 取类别置信度最大值 // 低于阈值则跳过 // 解码 cx, cy, w, h // 保存 mask 系数(32 个浮点) } // NMS,去掉重叠框 // 对每个剩余目标合成 mask // mask 形状从 proto 分辨率 resize 到原图分辨率这一步最消耗性能的是 mask 合成和 resize。我实测 160×160 的 proto 图,对每个目标做一次加权和,如果目标数量十几二十个,耗时会涨到 20ms 左右。优化思路是:只对最终通过 NMS 的目标做 mask 合成,不要对全部 8400 个候选做;另外 resize 时可以用 NCNN 的resize或 OpenCV 的resize,选双线性插值,比最近邻边缘更平滑,视觉效果好很多。
渲染方面,不建议在 native 层直接拼一张大透明图再传回 Java,这样有严重的跨层性能损失。我的做法是 native 层把 mask 画进一个固定大小的int[]bitmap 像素缓冲,然后通过AndroidBitmap_lockPixels直接写入一个Bitmap,一次性返回给上层。整个渲染过程不要走Bitmap.createBitmap反复分配。
4. 性能调优与实测数据:从卡顿到 25FPS 的优化路径
整个链路跑通以后,真正的挑战在于性能。我第一版实现就是“能跑、能出分割效果”,但帧率只有 8~10FPS,画面肉眼可见地延迟。后来我通过系统化调优,把骁龙 778G 上的帧率稳定在 20FPS 以上,部分场景接近 25FPS。下面按优化路径展开。
4.1 明确模型运行后端:CPU 还是 Vulkan GPU
NCNN 在安卓端有两种主要运行方式:CPU 和 Vulkan GPU。use_vulkan_compute开启后,NCNN 会把支持 GPU 的层(卷积、激活、池化等)提交给 GPU 执行。我用的是:
net.opt.use_vulkan_compute = useGpu; net.opt.use_fp16_arithmetic = true; net.opt.use_fp16_storage = true;注意use_fp16_arithmetic和use_fp16_storage的区别。前者是做算术运算时用 FP16,后者是存储权重时用 FP16。不要小看这个配置,很多设备上 FP16 能带来接近 30% 的提速,而且精度损失几乎不可感知。
实测下来,Vulkan 并非在所有机型上都是最优解。中高端芯片上,GPU 加速收益明显;但在低端机上,Vulkan 的初始化耗时和驱动兼容性问题反而会让帧率更差。我建议提供一个开关,让用户在设置页选择“CPU/GPU 自动”或“强制 GPU”,同时内部做一次设备 GPU 能力探测,不支持 Vulkan 时自动回退 CPU。NCNN 自带的ncnn::get_gpu_count()可以用来判断设备是否支持。
还有一个容易忽略的点:Vulkan 并行环境下,Extractor的输入输出数据的生命周期管理更严格。如果输入ncnn::Mat的数据指针在某些层执行时被转换或内部复用,可能导致输出异常。经验是每个extract调用之间,确保输入 Mat 在 extractor 内部阶段一直存活,不要提前释放。
4.2 输入尺寸、后处理并发与渲染帧率联动
输入尺寸是性价比最高的调节手段。YOLOv8 的输入最好保持 32 的倍数(因为模型下采样到 1/32 分辨率)。我当时测了 640、512、384、320 四档:
- 640×640:推理约 90~110ms,后处理约 25ms,总帧率约 7~9FPS
- 512×512:推理约 50~60ms,后处理约 15~20ms,总帧率约 13~15FPS
- 384×384:推理约 35~40ms,后处理约 12ms,总帧率约 18~20FPS
- 320×320:推理约 25~30ms,后处理约 10ms,总帧率约 22~26FPS
可以看到,从 640 降到 320,帧率提升了接近 3 倍。对很多业务场景来说,320 的分割边界“够用但不够精细”,如果 App 主要服务对象是非苛刻的使用者,完全可以用 320。我自己最后妥协在 512 和 384 之间,实时预览用 384,保存截图时才用 512 重新推理,体验和效果兼顾。
后处理也做了并发优化。NMS 和 mask 合成只依赖推理结果,不依赖摄像头采集,所以放到了独立线程,与下一帧的推理并发执行。这样即使后处理耗时 20ms,也不会阻塞到下一帧推理启动,帧率提升立竿见影。如果你用std::thread,注意给线程设置合理优先级并用std::atomic控制状态,别在后台线程里弹出 Android Toast 这类 UI 操作。
4.3 隐藏的无谓开销:日志打印和内存拷贝
很多人在调性能时忽略了一个问题:每帧都打印 FPS 日志。Log.i和__android_log_print在 native 层调用时,如果每帧输出一条长日志,会导致 I/O 阻塞,实测能吃掉 2~3ms 甚至更多。建议日志开关做成宏或者 flag,上线前全部关掉。另外,图像在 Java 层和 native 层之间来回传递如果有大量copyOfRange、System.arraycopy,也可能成为瓶颈。我最终把 YUV 数据以ByteBuffer形式直接传 JNI 地址,绕开 Java 数组拷贝,节省了不少开销。
还有一个小技巧:如果最终渲染是SurfaceView而不是ImageView,尽量避免使用Canvas.drawBitmap把整张图绘制两次。可以先在 native 层把分割蒙版画入一张透明底 Bitmap,再用硬件加速的Canvas绘制到 Surface,中间少一次软件渲染。框架选对以后,CPU 占用能降低 10% 左右。
5. 常见问题与排查技巧实录
这里我整理了一份安卓端部署 YOLOv8n-seg 时最高频出现的问题清单,供大家按图索骥。
5.1 模型已加载,但推理结果为空
现象是网络跑通了,输出数据也存在,但检测不到任何目标,或者置信度全部接近 0。这个问题九成出在输入预处理上——归一化方式不对、图像通道顺序错了、输入尺寸跟模型不匹配。我之前就写过把 RGBA 数据当成 RGB 传入,结果所有目标都被识别成背景。
排查步骤:先用 PC 端 NCNN Python 脚本验证同一张图和同一个模型,如果 PC 端正常,说明模型转换没问题,问题一定出在安卓端的输入数据;如果 PC 端同样为空,那就回头检查 ONNX 转换、模型权重路径。另外,打印一下ncnn::Mat的w/h/c,确认输入尺寸是预期值,比如 512×512 输入,w=512, h=512, c=3,而不要传成 512×3。
5.2 输出张量的维度跟预期不一致怎么办
有时候导出 ONNX 时 opset 版本不同,输出维度会变成 (1, 8400, 116) 而不是 (1, 116, 8400),甚至 proto 输出分辨率变成 1/8 而不是 1/4。这种问题没有统一答案,只能打印输出张量维度,根据实际布局写后处理代码。拿 NCNN 举例,ncnn::Mat的c是通道数,h是行数,w是宽,分别等于 ONNX 里的 C、H、W。cstep是通道间偏移,如果是一维大张量,可能cstep和w*h不同,遍历时务必带上cstep,不要用w*h去算偏移。
如果多次转换输出布局都很乱,我建议在导出时固定 ultralytics 版本,并在转换脚本里加一个断言,检查输出维度,不满足条件就直接报错,避免把错的模型带进安卓工程。
5.3 内存暴涨或帧率越来越低
有一种典型情况是运行一段时间后内存持续上涨,最终 OOM。这通常不是模型本身的问题,而是后处理阶段不断创建Bitmap,或者 NMS 的结果容器没有清空。注意Bitmap在 native 层创建后,如果不在 native 层释放,只能等 Java GC,但 GC 在 native 层不可见,容易堆积。我的做法是在 Java 层统一管理 Bitmap 计数,超过 N 帧就强制recycle,或者直接用 Android 的ImageReader配合 Surface 直接渲染,避免 Bitmap 频繁创建。
推理速度越来越慢则可能是 GPU 资源没有释放。开 Vulkan 后,如果创建了大量ncnn::VkCompute对象而忘记销毁,显存会持续占用。建议在设置页放置“重置推理引擎”按钮,方便现场调试时快速释放资源。
5.4 分割蒙版有横条纹或明显错位
这个问题多半是 proto mask 的 stride 或 resize 逻辑错了。proto mask 是在模型内部下采样得到的,输入 640 时输出 160×160,输入 512 时输出 128×128。如果你把模型输入改小,但仍在后处理里假设 proto 是 160×160,错位就不可避免。正确做法是从ncnn::Mat里直接读w/h,动态判断 proto 分辨率,别写死。另一个可能原因是没有对 mask 做sigmoid就直接二值化,导致概率值取值不对,边缘出现条纹。确保公式是sigmoid(weighted_sum)再对比阈值。
6. 后续扩展方向与个人实操心得
这版方案跑通后,我还有几个想继续做的方向。一是对自定义数据集做增量训练,目前 YOLOv8 官方支持从预训练权重继续训练,只要把数据集整理成 COCO 或 YOLO 分割格式,跑几个 epoch 就能在业务场景下获得更好的效果,模型体积基本不变。二是把后处理里的 mask 上采样从 CPU 移到 GPU 上,用 Vulkan 的 compute shader 做并行 resize,理论上能再省 5~8ms,让实时分割在低端机上也能跑满 25FPS。三是尝试 INT8 量化,配合针对性的校准集,看看能不能在精度损失可接受的前提下,把模型进一步压到 2MB 内,为一些存储极小的 IoT 设备做铺垫。
最后分享几个实操中的体会。第一,别被模型大小迷惑,4.8MB 是指文件体积,不是内存占用,运行时还需要为输入输出张量和中间特征分配显存,设计 App 时别把内存预算卡得太死。第二,后处理代码一定要写单元测试,我见过太多“模型能跑但结果一团糟”的案例,最后都是后处理逻辑写错了,光靠肉眼看很难发现。第三,性能调优要有量化目标,不要追求极限帧率而牺牲稳定性,帧率在 20FPS 以上、连续运行 30 分钟不崩溃、内存峰值不超过 300MB,这三个指标同时满足,才算是一个能交付的安卓端模型。边缘AI 这条路没有银弹,但它真正难的地方不在模型本身,而在于把模型放到真实设备的每一环“接缝”上。希望这篇文章能帮你少踩几个我踩过的坑。