news 2026/4/18 8:39:41

Compose: Android整合Yolo26e模型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Compose: Android整合Yolo26e模型

还记得上一篇咱们聊的Android整合Yolo模型吗?当时用 TensorFlow Lite 在 Android 里整了个 YOLO 模型,但是留了个非常关键的问题没解决——

那就是:YOLO26 只能识别 80 种对象,那 80 种之外的东西咋办?总不能让手机变成"睁眼瞎"吧?

所以当时我提了两个后续整明白的事儿:

  1. 咋给模型做再训练?
  2. YOLO 家族的新成员 YOLOE(号称"实时感知一切"),这货能识别任何对象,比传统 YOLO 模型强多了,咋整合到 Android 里?

今天重点搞定第二个问题——如何把这个号称能识别万物的 YOLOE 模型塞进手机里,让APP也用明清灵水洗一洗眼睛!

要把 YOLOE 整到 Android 里,得先想明白俩事儿:

  1. 选哪个版本的模型?
  2. 用啥方法把模型整合到 Android 里?

1. YOLOE——万物识别小能手

YOLOE 这名字听着就牛逼,直译过来就是"实时感知一切"!这货专门为开放词汇表检测和分割而生,跟之前那些只能识别固定类别的 YOLO 模型完全不是一个量级——它能用文本、图像或者自带的词汇表当提示,实时识别任何你能想到的对象!

yoloe-seg-pf——懒人福音版

这里的"pf"是"prompt free"的意思,翻译过来就是"无提示词"。简单说就是,你啥都不用告诉它,直接扔张图片过去,它就能给你把里面的东西都认出来!
而且模型内置了4585种不同的对象类别,普通场景绝对够用。

fromultralyticsimportYOLOE# 加载无提示词模型model=YOLOE('yoloe-26l-seg-pf.pt')# 直接预测图片results=model.predict('bus.jpg')# 显示结果results[0].show()

YOLOE-seg——精准打击版

要是只想识别特定的东西(比如只看人和公交车),那就用 YOLOE-seg 模型,这货可以接受你指定的类别列表,精准定位你想看的东西,绝不浪费算力!

fromultralyticsimportYOLOE# 加载标准模型model=YOLOE('yoloe-26l-seg.pt')# 设置只识别人和公交车model.set_classes(['person','bus'])# 开始预测results=model.predict('bus.jpg')

2. 模型转换——踩坑记

上回整 YOLO 模型的时候,是把 PyTorch 模型转成 TensorFlow Lite 模型,当时可是花费了一坤日才学会的。

尝试转 TensorFlow Lite——失败

结果到 YOLOE 这儿,这招不灵了!直接转 TFLite 模型?门儿都没有!
转的时候直接报错,说什么"reshape 张量维度不匹配"。问了问千问才明白:

YOLOE 的 -seg 模型带了个实例分割头(mask head),里面用了动态 reshape,导出 ONNX 时没把维度固定死,导致 onnx2tf 转换时直接维度不匹配。

得,此路不通,试试换个法呗。

转向 ONNX——成功(但过程坎坷)

于是我想,不整这么复杂,ONNX 模型行不行?查了查官方文档,发现微软出的 ONNX Runtime 框架可以在 Android 上跑 ONNX 模型!

说干就干,转 ONNX 模型应该不难吧?就几行代码的事儿:

fromultralyticsimportYOLOE model=YOLOE('yoloe-26l-seg-pf.pt')model.export(format='onnx')

结果~~又报错了!千问给的解释是:

从你的模型名 YOLOE-26n-seg-pf.pt 可以看出:

  1. seg:表示支持实例分割;
  2. pf:表示 Prompt-Free 模式(即无需文本/视觉提示,自动检测所有物体)。
    在 Prompt-Free 模式下,YOLOE 可能禁用了文本提示相关的分类头(text prompt head),导致 cls_head 或 bn_head 被设为 None。而当前导出流程中的 fuse() 函数 未正确处理这种“部分 head 缺失”的情况,直接对 None 做了 zip,从而崩溃。

解决方案就是,在导出ONNX模型的时候,禁用文本提示相关的分类头(text prompt head)。

defexportModel(modelname):model=YOLOE(modelname,task="detect")# 禁用整个模型的 fuse 行为original_fuse=getattr(model.model,'fuse',None)iforiginal_fuseisnotNone:model.model.fuse=lambda:model.model# 返回自身,不 fuse# 同时禁用 head 的 fuse(双重保险)head=model.model.model[-1]ifhasattr(head,'fuse'):head.fuse=lambda*args,**kwargs:Nonemodel.export(format="onnx",half=True,dynamic=False,simplify=True)

对了,这里的half=true参数是为了减小模型大小,导出的是 FP16(float16)精度的模型,跟上次转 TFLite 模型时用的 float16 效果一样,省空间又不咋影响性能——简直是移动端的福音!

3. Android 中整合 ONNX Runtime——实战篇

第一步:添加依赖

先给项目加个 ONNX Runtime 依赖,基本上就像给手机装个插件一样简单:

libs.versions.toml

[versions] onnxruntimeAndroid = "1.23.2" onnxruntime-android = { module = "com.microsoft.onnxruntime:onnxruntime-android", version.ref = "onnxruntimeAndroid" }

build.gradle.kts

dependencies{implementation(libs.onnxruntime.android)}

第二步:放模型文件

把转好的 ONNX 模型和分类文件丢到 assets 目录里:

对了,这里的tag_list_chinese.txt是从官方 GitHub 仓库下载的中文分类文件。

第三步:写识别代码

图像处理的部分上回已经聊过了,这次就不啰嗦了,直接上核心代码。

ONNX Runtime 用的是 OrtSession 来跑模型,咱整一个OnnxYoloeModel类:

OnnxYoloeModel.kt

packagecn.mengfly.whereareyou.core.detectimportai.onnxruntime.OnnxTensorimportai.onnxruntime.OnnxValueimportai.onnxruntime.OrtEnvironmentimportai.onnxruntime.OrtSessionimportandroid.content.Contextimportandroid.graphics.Bitmapimportandroid.graphics.RectFimportcn.mengfly.whereareyou.core.loadClassesimportcn.mengfly.whereareyou.core.preProcessImageimportjava.io.ByteArrayOutputStreamimportjava.io.InputStreamimportjava.nio.FloatBufferobjectOnnxYoloeModel:DetectModel{privateconstvalINPUT=640// 输入图片大小privateconstvalMODEL_PATH="yoloe-26l-seg-pf.onnx"// 模型路径privatelateinitvarsession:OrtSession// ONNX 会话privatelateinitvarenv:OrtEnvironment// ONNX 环境privatelateinitvarclasses:List<String>// 分类列表privateconstvalCONF_THRESHOLD=0.25f// 置信度阈值/** * 模型是否初始化完成 */overridevalisInit:Booleanget()=::session.isInitializedoverridesuspendfuninit(context:Context){// 加载分类文件classes=loadClasses(context,"tag_list_chinese.txt")// 加载 onnx 模型env=OrtEnvironment.getEnvironment();context.assets.open(MODEL_PATH).use{valreadAllBytes=readAllBytes(it)session=env.createSession(readAllBytes,OrtSession.SessionOptions())}}overridesuspendfundetect(bitmap:Bitmap):List<DetectionResult>{// 预处理图像:缩放到 640x640,归一化到 [0, 1] 范围// 流程和上篇文章一样,就是把逻辑封装了一下valresized=bitmap.preProcessImage(INPUT)// 由于预处理后的图像数据为 HWC(height, width, channel)格式// 而ONNX模型的输入要求为(batch, channel, height, width)格式// 所以需要先将HWC转换为CHW(channel, height, width)格式valchwData=hwcToChw(resized.tensorBuffer.floatArray,INPUT,INPUT,3)// 构建输入张量valinputTensor=OnnxTensor.createTensor(env,FloatBuffer.wrap(chwData),longArrayOf(1,3,INPUT.toLong(),INPUT.toLong()))// 运行模型valinputs=mapOf<String,OnnxTensor>(session.inputNames.toList()[0]toinputTensor)valresult=session.run(inputs,OrtSession.RunOptions())// 解析输出,过滤掉低置信度的结果returnparseOutput(result[0]).applyNMS()// 非极大值抑制,去除重叠的框}// HWC 转 CHW 格式的工具函数funhwcToChw(hwcData:FloatArray,height:Int,width:Int,channels:Int):FloatArray{valchwData=FloatArray(hwcData.size)for(hin0until height){for(win0until width){for(cin0until channels){valhwcIndex=(h*width+w)*channels+cvalchwIndex=c*height*width+h*width+w chwData[chwIndex]=hwcData[hwcIndex]}}}returnchwData}// 读取输入流的工具函数funreadAllBytes(inputStream:InputStream):ByteArray{valbuffer=ByteArrayOutputStream()valdata=ByteArray(1024)varbytesRead:Intwhile(inputStream.read(data).also{bytesRead=it}!=-1){buffer.write(data,0,bytesRead)}returnbuffer.toByteArray()}/** * 解析模型输出 * 注:这里只处理了检测框、分类和置信度,分割输出没处理 */privatefunparseOutput(output:OnnxValue):List<DetectionResult>{valtensor=outputasOnnxTensorvaldetectResult=(tensor.valueasArray<*>)[0]asArray<*>valresult=mutableListOf<DetectionResult>()for(itemindetectResult){valdetectRes=itemasFloatArray// 提取检测信息valleft=detectRes[0]valtop=detectRes[1]valright=detectRes[2]valbottom=detectRes[3]valconfidence=detectRes[4]valclassType=detectRes[5].toInt()// 跳过低置信度的结果if(confidence<CONF_THRESHOLD){continue}// 获取类别名称valclassStr:String=if(classType>=classes.size){"unknown"// 未知类别}else{classes[classType]}// 添加到结果列表result.add(DetectionResult(classStr,confidence,RectF(left,top,right,bottom)))}returnresult}}

整合识别结果

yoloe 虽然能识别的东西变多了,但是误识别的情况也跟着多了——有时候同一个东西,它能给你识别成好几种不同的物体,这就很尴尬了……

细心的伙伴应该注意到了,代码里调用了applyNMS方法,这玩意儿是干嘛的?

简单说,就是"非极大值抑制"——过滤掉那些重叠的、置信度低的检测框。但光有这还不够,我还做了个小优化:

如果两个不同类别的检测框几乎完全重合(IOU > 0.99),我就把置信度低的那个标记为"低可信度",显示的时候就能区分开了!

DetectionResult.kt

importandroid.graphics.RectFimportandroidx.compose.runtime.getValueimportandroidx.compose.runtime.mutableStateOfimportandroidx.compose.runtime.setValuedataclassDetectionResult(valclassType:String,// 类别名称valconfidence:Float,// 置信度valboundingBox:RectF// 检测框){varisSelectedbymutableStateOf(false)// 是否被选中显示varlowConfidencebymutableStateOf(false)// 是否是低可信度结果}/** * 非极大值抑制(NMS):去除重叠的检测框与标记低可信度结果 */funList<DetectionResult>.applyNMS(iouThreshold:Float=0.45f):List<DetectionResult>{// 先按置信度从高到低排序valsorted=sortedByDescending{it.confidence}valkeep=mutableListOf<DetectionResult>()for(currinsorted){varkeepCurr=truefor(keptinkeep){if(curr.classType==kept.classType){// 相同类别,重叠度太高就干掉if(calculateIOU(curr.boundingBox,kept.boundingBox)>iouThreshold){keepCurr=falsebreak}}else{// 不同类别,但框几乎重合,标记为低可信度if(calculateIOU(curr.boundingBox,kept.boundingBox)>0.99f){curr.lowConfidence=truebreak}}}if(keepCurr)keep.add(curr)}returnkeep}/** * 计算交并比(IOU):判断两个框重叠程度 */privatefuncalculateIOU(box1:RectF,box2:RectF):Float{// 计算重叠区域的坐标valintersectLeft=maxOf(box1.left,box2.left)valintersectTop=maxOf(box1.top,box2.top)valintersectRight=minOf(box1.right,box2.right)valintersectBottom=minOf(box1.bottom,box2.bottom)// 计算重叠面积valintersectArea=maxOf(0f,intersectRight-intersectLeft)*maxOf(0f,intersectBottom-intersectTop)// 计算两个框的总面积valbox1Area=(box1.right-box1.left)*(box1.bottom-box1.top)valbox2Area=(box2.right-box2.left)*(box2.bottom-box2.top)// 交并比 = 重叠面积 / (总面积 - 重叠面积)returnintersectArea/(box1Area+box2Area-intersectArea)}

4. 识别结果

至于具体怎么调用模型,上一篇已经聊得很详细了,这里就不啰嗦了。不过这次整合的时候,做了两个角度的优化:

  1. 智能显示:yoloe 识别的东西太多了,要是全显示出来页面得乱成一锅粥。所以改成了手动选择——想看哪个点哪个,清爽又方便!

  2. 模型切换:我把之前的 TensorFlow 模型也保留了,抽象了一个DetectModel接口,想切哪个模型就切哪个!

话不多说,直接看效果。

对了,模型和源码咱们已经打包上传到网盘了,具体链接就在我的这篇公众号文章里,需要的小伙伴自己去拿哈!

https://mp.weixin.qq.com/s/EfB4Gd3oS9woDPWicNxUvw


总结一下:这次成功把 YOLOE 这个号称"识别万物"的模型整到了 Android 里,解决了之前 YOLO 模型只能识别 80 种对象的问题。虽然过程中踩了不少坑,但最终效果还是挺不错的!

各位小伙伴要是有什么问题,欢迎在评论区留言,一起交流学习!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/18 7:57:06

mitt 通信

mitt 简介在 Vue 应用中&#xff0c;我们经常遇到这样的情况&#xff1a;两个组件之间没有直接的父子关系&#xff0c;但需要共享数据或者互相通信。比如&#xff0c;一个页面中的头部组件需要知道用户点击了侧边栏的某个菜单项。这时候&#xff0c;使用 props 和事件会非常麻烦…

作者头像 李华
网站建设 2026/4/18 5:41:36

能碳管理平台:引领工业企业与园区数字化能碳管理新方向

一、政策背景为加强工业节能降碳管理&#xff0c;推进数字技术赋能绿色低碳转型&#xff0c;支撑构建系统完备的碳排放双控制度体系&#xff0c;工信部近日印发了《工业企业和园区数字化能碳管理中心建设指南》。安科瑞依据《指南》&#xff0c;打造了能碳管理解决方案&#xf…

作者头像 李华
网站建设 2026/4/18 7:53:04

从经典到量子:理解 |0⟩ 与 |1⟩ 的基石意义

在传统计算机的世界里&#xff0c;一切信息都构建在两个泾渭分明的状态之上&#xff1a;0 和 1。电路的通断、电压的高低、磁极的方向&#xff0c;这些物理实现最终都编码为二进制序列。然而&#xff0c;当我们踏入量子计算的领域&#xff0c;这套运行了数十年的逻辑迎来了根本…

作者头像 李华
网站建设 2026/4/18 5:44:02

UART串口不定长数据接收方法

一、基本概念与问题 在嵌入式系统中&#xff0c;串口&#xff08;UART&#xff09;通信时&#xff0c;数据通常以不定长的“帧”为单位发送。串口硬件本身只能识别单个字节的接收完成&#xff0c;无法自动判断一帧数据何时开始和结束。因此&#xff0c;需要通过软件方法来解决…

作者头像 李华
网站建设 2026/4/17 21:37:35

ZigBee隧道定位技术:赋能地下工程安全高效管控

Zigbee隧道定位技术概述 Zigbee隧道定位是一种基于Zigbee无线通信技术的定位方案&#xff0c;适用于隧道、矿井等封闭或半封闭环境。Zigbee因其低功耗、低成本、自组网能力&#xff0c;成为隧道定位的理想选择。该技术通过部署Zigbee节点&#xff08;如信标或锚点&#xff09;…

作者头像 李华