核心内容摘要
Keil开发中#ifndef、#define、#endif的3个实际应用场景(附代码示例)
还记得上一篇咱们聊的Android整合Yolo模型吗当时用 TensorFlow Lite 在 Android 里整了个 YOLO 模型但是留了个非常关键的问题没解决——那就是YOLO26 只能识别 80 种对象那 80 种之外的东西咋办总不能让手机变成睁眼瞎吧所以当时我提了两个后续整明白的事儿咋给模型做再训练YOLO 家族的新成员 YOLOE号称实时感知一切这货能识别任何对象比传统 YOLO 模型强多了咋整合到 Android 里今天重点搞定第二个问题——如何把这个号称能识别万物的 YOLOE 模型塞进手机里让APP也用明清灵水洗一洗眼睛要把 YOLOE 整到 Android 里得先想明白俩事儿选哪个版本的模型用啥方法把模型整合到 Android 里
YOLOE——万物识别小能手YOLOE 这名字听着就牛逼直译过来就是实时感知一切这货专门为开放词汇表检测和分割而生跟之前那些只能识别固定类别的 YOLO 模型完全不是一个量级——它能用文本、图像或者自带的词汇表当提示实时识别任何你能想到的对象yoloe-seg-pf——懒人福音版这里的pf是prompt free的意思翻译过来就是无提示词。
简单说就是你啥都不用告诉它直接扔张图片过去它就能给你把里面的东西都认出来而且模型内置了4585种不同的对象类别普通场景绝对够用。
fromultralyticsimportYOLOE# 加载无提示词模型modelYOLOE(yoloe-26l-seg-pf.pt)# 直接预测图片resultsmodel.predict(bus.jpg)# 显示结果results[0].show()YOLOE-seg——精准打击版要是只想识别特定的东西比如只看人和公交车那就用 YOLOE-seg 模型这货可以接受你指定的类别列表精准定位你想看的东西绝不浪费算力fromultralyticsimportYOLOE# 加载标准模型modelYOLOE(yoloe-26l-seg.pt)# 设置只识别人和公交车model.set_classes([person,bus])# 开始预测resultsmodel.predict(bus.jpg)
模型转换——踩坑记上回整 YOLO 模型的时候是把 PyTorch 模型转成 TensorFlow Lite 模型当时可是花费了一坤日才学会的。
尝试转 TensorFlow Lite——失败结果到 YOLOE 这儿这招不灵了直接转 TFLite 模型门儿都没有转的时候直接报错说什么reshape 张量维度不匹配。
问了问千问才明白YOLOE 的 -seg 模型带了个实例分割头mask head里面用了动态 reshape导出 ONNX 时没把维度固定死导致 onnx2tf 转换时直接维度不匹配。
得此路不通试试换个法呗。
转向 ONNX——成功但过程坎坷于是我想不整这么复杂ONNX 模型行不行查了查官方文档发现微软出的 ONNX Runtime 框架可以在 Android 上跑 ONNX 模型说干就干转 ONNX 模型应该不难吧就几行代码的事儿fromultralyticsimportYOLOE modelYOLOE(yoloe-26l-seg-pf.pt)model.export(formatonnx)结果~~又报错了千问给的解释是从你的模型名 YOLOE-26n-seg-pf.pt 可以看出seg表示支持实例分割pf表示 Prompt-Free 模式即无需文本/视觉提示自动检测所有物体。
在 Prompt-Free 模式下YOLOE 可能禁用了文本提示相关的分类头text prompt head导致 cls_head 或 bn_head 被设为 None。
而当前导出流程中的 fuse() 函数 未正确处理这种“部分 head 缺失”的情况直接对 None 做了 zip从而崩溃。
解决方案就是在导出ONNX模型的时候禁用文本提示相关的分类头text prompt head。
defexportModel(modelname):modelYOLOE(modelname,taskdetect)# 禁用整个模型的 fuse 行为original_fusegetattr(model.model,fuse,None)iforiginal_fuseisnotNone:model.model.fuselambda:model.model# 返回自身不 fuse# 同时禁用 head 的 fuse双重保险headmodel.model.model[-1]ifhasattr(head,fuse):head.fuselambda*args,**kwargs:Nonemodel.export(formatonnx,halfTrue,dynamicFalse,simplifyTrue)对了这里的halftrue参数是为了减小模型大小导出的是 FP16float16精度的模型跟上次转 TFLite 模型时用的 float16 效果一样省空间又不咋影响性能——简直是移动端的福音
Android 中整合 ONNX Runtime——实战篇第一步添加依赖先给项目加个 ONNX Runtime 依赖基本上就像给手机装个插件一样简单libs.versions.toml[versions] onnxruntimeAndroid
1.
2
2 onnxruntime-android { module com.microsoft.onnxruntime:onnxruntime-android, version.ref onnxruntimeAndroid }build.gradle.ktsdependencies{implementation(libs.onnxruntime.android)}第二步放模型文件把转好的 ONNX 模型和分类文件丢到 assets 目录里对了这里的tag_list_chinese.txt是从官方 GitHub 仓库下载的中文分类文件。
第三步写识别代码图像处理的部分上回已经聊过了这次就不啰嗦了直接上核心代码。
ONNX Runtime 用的是 OrtSession 来跑模型咱整一个OnnxYoloeModel类OnnxYoloeModel.ktpackagecn.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{privateconstvalINPUT640// 输入图片大小privateconstvalMODEL_PATHyoloe-26l-seg-pf.onnx// 模型路径privatelateinitvarsession:OrtSession// ONNX 会话privatelateinitvarenv:OrtEnvironment// ONNX 环境privatelateinitvarclasses:ListString// 分类列表privateconstvalCONF_THRESHOLD
25f// 置信度阈值/** * 模型是否初始化完成 */overridevalisInit:Booleanget()::session.isInitializedoverridesuspendfuninit(context:Context){// 加载分类文件classesloadClasses(context,tag_list_chinese.txt)// 加载 onnx 模型envOrtEnvironment.getEnvironment();context.assets.open(MODEL_PATH).use{valreadAllBytesreadAllBytes(it)sessionenv.createSession(readAllBytes,OrtSession.SessionOptions())}}overridesuspendfundetect(bitmap:Bitmap):ListDetectionResult{// 预处理图像缩放到 640x640归一化到 [0, 1] 范围// 流程和上篇文章一样就是把逻辑封装了一下valresizedbitmap.preProcessImage(INPUT)// 由于预处理后的图像数据为 HWCheight, width, channel格式// 而ONNX模型的输入要求为batch, channel, height, width格式// 所以需要先将HWC转换为CHWchannel, height, width格式valchwDatahwcToChw(resized.tensorBuffer.floatArray,INPUT,INPUT,
// 构建输入张量valinputTensorOnnxTensor.createTensor(env,FloatBuffer.wrap(chwData),longArrayOf(1,3,INPUT.toLong(),INPUT.toLong()))// 运行模型valinputsmapOfString,OnnxTensor(session.inputNames.toList()[0]toinputTensor)valresultsession.run(inputs,OrtSession.RunOptions())// 解析输出过滤掉低置信度的结果returnparseOutput(result[0]).applyNMS()// 非极大值抑制去除重叠的框}// HWC 转 CHW 格式的工具函数funhwcToChw(hwcData:FloatArray,height:Int,width:Int,channels:Int):FloatArray{valchwDataFloatArray(hwcData.size)for(hin0until height){for(win0until width){for(cin0until channels){valhwcIndex(h*widthw)*channelscvalchwIndexc*height*widthh*widthw chwData[chwIndex]hwcData[hwcIndex]}}}returnchwData}// 读取输入流的工具函数funreadAllBytes(inputStream:InputStream):ByteArray{valbufferByteArrayOutputStream()valdataByteArray(
varbytesRead:Intwhile(inputStream.read(data).also{bytesReadit}!-
{buffer.write(data,0,bytesRead)}returnbuffer.toByteArray()}/** * 解析模型输出 * 注这里只处理了检测框、分类和置信度分割输出没处理 */privatefunparseOutput(output:OnnxValue):ListDetectionResult{valtensoroutputasOnnxTensorvaldetectResult(tensor.valueasArray*)[0]asArray*valresultmutableListOfDetectionResult()for(itemindetectResult){valdetectResitemasFloatArray// 提取检测信息valleftdetectRes[0]valtopdetectRes[1]valrightdetectRes[2]valbottomdetectRes[3]valconfidencedetectRes[4]valclassTypedetectRes[5].toInt()// 跳过低置信度的结果if(confidenceCONF_THRESHOLD){continue}// 获取类别名称valclassStr:Stringif(classTypeclasses.size){unknown// 未知类别}else{classes[classType]}// 添加到结果列表result.add(DetectionResult(classStr,confidence,RectF(left,top,right,bottom)))}returnresult}}整合识别结果yoloe 虽然能识别的东西变多了但是误识别的情况也跟着多了——有时候同一个东西它能给你识别成好几种不同的物体这就很尴尬了……细心的伙伴应该注意到了代码里调用了applyNMS方法这玩意儿是干嘛的简单说就是非极大值抑制——过滤掉那些重叠的、置信度低的检测框。
但光有这还不够我还做了个小优化如果两个不同类别的检测框几乎完全重合IOU
99我就把置信度低的那个标记为低可信度显示的时候就能区分开了DetectionResult.ktimportandroid.graphics.RectFimportandroidx.compose.runtime.getValueimportandroidx.compose.runtime.mutableStateOfimportandroidx.compose.runtime.setValuedataclassDetectionResult(valclassType:String,// 类别名称valconfidence:Float,// 置信度valboundingBox:RectF// 检测框){varisSelectedbymutableStateOf(false)// 是否被选中显示varlowConfidencebymutableStateOf(false)// 是否是低可信度结果}/** * 非极大值抑制NMS去除重叠的检测框与标记低可信度结果 */funListDetectionResult.applyNMS(iouThreshold:Float
45f):ListDetectionResult{// 先按置信度从高到低排序valsortedsortedByDescending{it.confidence}valkeepmutableListOfDetectionResult()for(currinsorted){varkeepCurrtruefor(keptinkeep){if(curr.classTypekept.classType){// 相同类别重叠度太高就干掉if(calculateIOU(curr.boundingBox,kept.boundingBox)iouThreshold){keepCurrfalsebreak}}else{// 不同类别但框几乎重合标记为低可信度if(calculateIOU(curr.boundingBox,kept.boundingBox)
99f){curr.lowConfidencetruebreak}}}if(keepCurr)keep.add(curr)}returnkeep}/** * 计算交并比IOU判断两个框重叠程度 */privatefuncalculateIOU(box1:RectF,box2:RectF):Float{// 计算重叠区域的坐标valintersectLeftmaxOf(box
left,box
left)valintersectTopmaxOf(box
top,box
top)valintersectRightminOf(box
right,box
right)valintersectBottomminOf(box
bottom,box
bottom)// 计算重叠面积valintersectAreamaxOf(0f,intersectRight-intersectLeft)*maxOf(0f,intersectBottom-intersectTop)// 计算两个框的总面积valbox1Area(box
right-box
left)*(box
bottom-box
top)valbox2Area(box
right-box
left)*(box
bottom-box
top)// 交并比 重叠面积 / (总面积 - 重叠面积)returnintersectArea/(box1Areabox2Area-intersectArea)}
识别结果至于具体怎么调用模型上一篇已经聊得很详细了这里就不啰嗦了。
不过这次整合的时候做了两个角度的优化智能显示yoloe 识别的东西太多了要是全显示出来页面得乱成一锅粥。
所以改成了手动选择——想看哪个点哪个清爽又方便模型切换我把之前的 TensorFlow 模型也保留了抽象了一个DetectModel接口想切哪个模型就切哪个话不多说直接看效果。
对了模型和源码咱们已经打包上传到网盘了具体链接就在我的这篇公众号文章里需要的小伙伴自己去拿哈https://mp.weixin.qq.com/s/EfB4Gd3oS9woDPWicNxUvw
总结一下这次成功把 YOLOE 这个号称识别万物的模型整到了 Android 里解决了之前 YOLO 模型只能识别 80 种对象的问题。
虽然过程中踩了不少坑但最终效果还是挺不错的各位小伙伴要是有什么问题欢迎在评论区留言一起交流学习