您现在的位置是:网站首页> AI人工智能
YOLO开发技术积累
- AI人工智能
- 2026-07-11
- 1520人已阅读
YOLO开发技术积累

YOLO识别钉子并获得钉子长度(分步多级识别)
YOLO 模型在 Android studio 中详细使用步骤及示例代码
AI辅助学习YOLO
YOLO 模型在 Android studio 中详细使用步骤及示例代码
Q:YOLO 模型默认能识别哪些东西
A:...
YOLOv5/v8/v10/v11 官方自带预训练模型,默认基于 COCO 数据集训练,固定只能识别 80 类日常生活常见物体,没在这 80 类里的东西一律认不出来(比如安全帽、螺丝、垃圾桶、烟头、口罩全都识别不到,必须自己重新训练)。
一、80 类完整中文清单(按分组好记)
0 人物(人流量统计只用这个)
0:person 人
1-8 交通工具
1:自行车、2:轿车、3:摩托车、4:飞机、5:公交车、6:火车、7:卡车、8:船
9-13 道路公共设施
9:红绿灯、10:消防栓、11:禁止通行标志牌、12:停车计费桩、13:长椅
14-23 动物
14:鸟、15:猫、16:狗、17:马、18:绵羊、19:牛、20:大象、21:熊、22:斑马、23:长颈鹿
24-38 随身物品 + 运动器材
24:背包、25:雨伞、26:手提包、27:领带、28:行李箱
29:飞盘、30:滑雪板、31:单板雪板、32:球类、33:风筝
34:棒球棍、35:棒球手套、36:滑板、37:冲浪板、38:网球拍
39-55 餐具 + 食物水果
39:瓶子、40:红酒杯、41:水杯、42:叉子、43:刀、44:勺子、45:碗
46:香蕉、47:苹果、48:三明治、49:橙子、50:西兰花、51:胡萝卜
52:热狗、53:披萨、54:甜甜圈、55:蛋糕
56-72 家居家电家具
56:椅子、57:沙发、58:盆栽、59:床、60:餐桌、61:马桶
62:电视、63:笔记本电脑、64:鼠标、65:遥控器、66:键盘、67:手机
68:微波炉、69:烤箱、70:烤面包机、71:水槽、72:冰箱
73-79 小件杂物
73:书本、74:时钟、75:花瓶、76:剪刀、77:毛绒玩具、78:吹风机、79:牙刷
二、关键误区(结合你做人流量场景)
1.默认模型只能识别 “人”,识别不了行人、顾客、游客细分
所有人统一标签都是 person,想要区分大人小孩 / 男女,必须重新训练自定义数据集。
2.工业、监控常用物品默认全都没有
安全帽、口罩、垃圾桶、电动车、围栏、电线、零件、烟火、宠物笼、快递箱,COCO 里不存在,原生 YOLO 完全识别不到。
3.树木、墙面、地面、天空、道路不属于任何类别,不会被框选
4.默认 80 类固定死,不能新增;想识别新物品必须标注图片重新训练
三、和千问多模态大模型对比(呼应前面问题)
YOLO 默认:封闭 80 类固定清单,只能框训练过的物体,不懂语义;
Qwen-VL 图文大模型:开放词汇,不用训练,你文字描述什么物体它就能定位识别,不受固定类别限制。
四、做人流量统计怎么用
只用编号 0 person,过滤其余 79 类,只追踪画面里的人,搭配 ByteTrack 做跨线计数,就是商场 / 小区人流量统计完整方案。
Q:YOLO如何根据人走向统计人流量
A:...
Q:你是一名AI领域的专家,你知道yolov6吗?
A:...
Q:如何训练自己的YOLOv6模型
A:...
Q:现实生活中用Yolov6的例子
A:...
Q:超市 / 便利店货架摄像头实时识别商品、空缺货位、摆放错乱 给出详细实现步骤及代码
A:...
Q:对摄像头有啥特殊要求吗?
A:...
Q:详细介绍训练,和训练数据,并给出例子数据
A:...
Q:yolov6成功的案例除了美团自身的
A:...
Q:训练出的模型只能用python调用吗
A:...
Q:如何导出onnx
A:...
Q:YOLO 和其他图像识别大模型区别比如千问
A:...
定位分工:特种兵 vs 全能顾问(最好理解的类比)
YOLO = 高速探测器(只干一件事:快速框物体)
核心目标:实时、精准标出画面里物体的方框 + 类别
1.只能识别提前标注训练过的固定种类:比如你只训了车、人、垃圾桶,陌生物品它识别不出来;
2.速度极快:一张图几十毫秒,摄像头 30 帧实时流无压力;
3.只会输出方框坐标 + 物体名字,不会理解关系、不会说话、不会推理;
举例子:
图片里一个人抱着猫,YOLO 只会分别框出「人」「猫」,不知道 “抱着” 这个动作;你问它这个人穿什么颜色衣服,它答不上来。
Qwen-VL(千问图文大模型)= 会思考看图聊天的顾问
核心目标:看懂画面、听懂文字提问、逻辑推理、自由描述
1.零样本识别:不用专门训练,你说 “找画面里戴黑框眼镜拿奶茶的女生”,它就能定位;
2.全局语义理解:看得懂动作、关系、情绪、文字、图表、手写笔记;
3.能直接输出自然语言回答,甚至写总结、分析、报表;
缺点:速度慢十几倍,一张图处理几百~上千毫秒,做实时监控会卡成 PPT。
举落地场景,一眼分清该用谁
场景 1:小区监控实时统计行人、车辆 → 选 YOLO
需要每秒 30 帧快速框人计数,低延迟,硬件便宜。
场景 2:上传一张发票,自动提取金额、税号并解读 → 选千问 VL
要读懂文字、区分字段、输出结构化文字,YOLO 做不到语义解析。
场景 3:工厂流水线检测零件缺口 → YOLO
固定几十种缺陷,实时高速抓拍。
场景 4:拍一张手绘草图,找出图里所有实木沙发 → 千问 VL
不用专门标注训练,靠文字指令检索物体。
黄金组合(工业最常用)
YOLO 先快速框出画面所有物体,裁剪局部区域丢给千问 VL 做深度语义判断:
比如监控先 YOLO 框出倒地的人,再交给千问分析:是老人、穿红色外套、是否需要报警,兼顾速度和理解力。
一句话总结核心差距
YOLO 是专用高速视觉探测器(CNN 卷积),只会快速框已知物体;
千问图文大模型是Transformer 双架构全能看图 AI,能看懂、思考、聊天,但慢、吃显卡;
两者不是替代关系,大多项目搭配使用。
Q:和 YOLO 相同的开源免费竞品有吗?
A:...
全部开源、免费、允许商用,和 YOLO 定位完全一致:实时视频目标检测、可训练自定义物体、支持导出 ONNX / 部署嵌入式,适配人流量统计、工业质检、安防监控场景。
一、同 CNN 卷积路线(和 YOLO 底层结构一致,上手逻辑接近,首选替代)
1. PP-YOLOE / PP-YOLOE+(百度飞桨,国内工业落地最多)
协议:Apache2.0,完全免费商用无限制
特点:无锚框、速度精度均衡,n/s/m/l/x 全套轻量化模型;原生支持 ByteTrack 跟踪、旋转框、分割、关键点;对国产 NPU(地平线、瑞芯微、昇腾)优化极好
优势:中文文档完善,国内工控 / 摄像头厂商大量内置;导出 ONNX/TensorRT 兼容性拉满
劣势:依赖 PaddlePaddle 框架,不能直接用 PyTorch 生态
2. DAMO-YOLO(阿里达摩院)
协议:Apache2.0 商用免费
核心亮点:神经网络自动搜索 NAS 轻量化骨干,同等速度精度高于 YOLOv8;推理延迟极低,适合低性能 CPU / 嵌入式
配套:自带完整训练、跟踪、导出流水线,适合门店人流、产线缺陷检测
3. YOLOX(旷世开源,无锚框标杆)
协议:Apache2.0,无商用约束
定位:极简无锚 YOLO 分支,去掉复杂锚框逻辑,小模型 YOLOX-Nano 仅 0.9M 参数,单片机都能跑
适合:极低算力设备、学术二次改造;缺点官方维护更新慢,生态不如 Ultralytics YOLO 完善
4. RTMDet(OpenMMLab 开源)
协议:Apache2.0
特点:MMDetection 工具箱内置,速度媲美 YOLO,支持检测 / 分割 / 关键点全套任务;模块化极强,方便自定义修改网络
适合:需要大量算法实验、多模型对比的研发场景
5. YOLO-Fastest(超轻嵌入式专用)
协议开源免费,针对 ARM/RK3588 / 树莓派深度优化
极致小体积,320 分辨率 CPU 可达 30 帧,只做纯检测,无分割 / 关键点,适合廉价监控主板
二、Transformer 实时检测竞品(全新技术路线,精度更强,适合人群密集遮挡场景)
YOLO 是 CNN,这一类用注意力全局建模,不用 NMS 后处理,多人重叠、遮挡漏检更少,做人流量统计误差更低。
1. RT-DETR / RT-DETRv2(百度开源,工业落地成熟)
协议 Apache2.0,免费商用
核心优势:端到端无 NMS,后处理无重复框;全局注意力,人群拥挤、远距离行人识别比 YOLO 准;同样支持 Nano/Large 全套尺寸
短板:同等尺寸比 YOLO 慢一点,低配 CPU 帧率偏低;带独显工控机首选替代 YOLO 做人流统计
2. RF-DETR(Roboflow,2026 最新 SOTA 实时 Transformer 检测器)
Nano/Large 版本 Apache2.0 免费商用;超大模型需企业授权
COCO 精度突破 60AP,实时推理,密集行人、复杂场景碾压 YOLO;原生配套跟踪算法,专门适配监控客流统计
适合:高精度需求门店、景区人流,算力充足设备
三、零样本开放词汇竞品(特殊场景:不用标注训练就能识别任意物体)
Grounding DINO
Apache2.0 开源免费商用
和上面所有模型区别:YOLO/RT-DETR 必须标注图片训练;Grounding DINO 输入文字就能找物体,零样本识别
局限:推理速度很慢,不适合实时视频人流统计,只适合静态图片识图检索
横向对比(针对你的店铺人流量场景)
| 模型 | 底层架构 | 速度 (CPU) | 人群遮挡表现 | 部署友好度 | 商用推荐度 |
|---|---|---|---|---|---|
| YOLOv8/v11 | CNN | 极快 | 一般,遮挡易丢 ID | 极高,一行代码训练跟踪导出 | ⭐⭐⭐⭐⭐(通用首选) |
| PP-YOLOE+ | CNN | 快 | 良好 | 国产硬件适配拉满 | ⭐⭐⭐⭐⭐(国产工控替换首选) |
| DAMO-YOLO | CNN | 很快 | 良好 | 轻量化优化强 | ⭐⭐⭐⭐ |
| YOLOX-Nano | CNN | 极快 | 一般 | 超小边缘设备 | ⭐⭐⭐ |
| RT-DETRv2 | Transformer | 中等 | 优秀,漏检少 | 无 NMS,计数更稳 | ⭐⭐⭐⭐⭐(人流密集门店) |
| RF-DETR-N | Transformer | 中等 | 顶级 | 高精度客流专用 | ⭐⭐⭐⭐(高端高精度场景) |
Q:YOLO 可以这样用吗?比如要获得钉子的头部到底部的像素长度,首先通过一个模型识别出所有钉子,让再讲每个钉子图像喂给识别钉子头部和底部的模型,获得头部底部坐标,然后计算长度
A:...
结论:完全可以,工业视觉里这套流程叫两级级联检测,大量用于五金、零件尺寸测量,思路逻辑没问题,但有更优方案,下面分通俗讲解、优缺点、优化方案、简易代码思路给你。
一、你的方案完整逻辑拆解(两级 YOLO 串联)
第一级 YOLO(粗检)
训练模型,类别只有 nail(钉子),在整张图里把所有钉子框出来;
裁剪每个钉子框对应的局部小图,过滤背景、无关杂物,减少第二级模型干扰。
第二级 YOLO(细检)
单独训练小模型,两个类别:nail_head 钉头、nail_tip 钉尖;
把裁剪后的单张钉子局部图输入二级模型,输出钉头、钉尖坐标。
像素长度计算
拿到钉头框中心点 (x1,y1)、钉尖框中心点 (x2,y2),欧式距离算出像素长度:
如果提前标定相机(像素转毫米比例),还能换算真实物理长度。
二、这套方案的优点
1.训练难度低
不用一张图标注大量点;一级只标整根钉子,二级只标局部钉头钉尖,标注成本分开,小数据集就能收敛。
2.抗干扰强
整张图有木板、螺丝、杂物时,一级先把钉子单独裁剪出来,二级模型不用处理复杂背景,误检大幅下降。
3.算力灵活拆分
一级用超轻 YOLO-Nano 快速筛选物体;二级小模型高精度精细定位,CPU 工控机也能跑。
三、原生短板(商用落地会遇到的坑)
1.两次推理,耗时翻倍
一张图先跑一次 YOLO,每个钉子裁剪后再单独跑一次模型,实时流水线场景帧率会下降。
2.物体倾斜、重叠容易失效
多根钉子堆叠时,一级裁剪框包含两根钉子,二级同时识别两组钉头钉尖,无法配对,分不清哪个头对应哪个尖,计算长度错乱。
3.框中心点误差
只用检测框中心计算长度精度有限;钉头 / 钉尖是方框,中心点不是真实边缘点,微小像素误差累积。
4.维护两套模型
需要保存、导出、部署两个 onnx 模型,参数调试、版本更新都要维护两份文件。
四、三种更优替代方案(推荐商用落地)
方案 1:单模型多类别(最优,推荐替换你的两级方案)
只训练一个 YOLO 模型,3 个类别:nail、nail_head、nail_tip
1.整张图一次推理,同时输出所有钉子、所有钉头、钉尖坐标;
2.通过坐标距离匹配:距离最近的钉头、钉尖归为同一根钉子;
3.一次推理完成,速度快,只维护一套模型。
方案 2:YOLO 检测 + 关键点检测(精度最高,尺寸测量首选)
YOLOv8/v10 原生支持关键点训练:
1.一个模型,类别 = 钉子;每个钉子标注 2 个关键点:钉头、钉尖;
2.推理直接输出每个钉子对应的两组精确坐标,天然一一配对,不存在匹配错乱;
3.关键点是像素级坐标,比框中心精准很多,长度测量误差最小。
方案 3:YOLO 裁剪 + 分割模型(边缘精准测量)
一级 YOLO 框出钉子,裁剪后用分割模型提取钉子轮廓,轮廓端点直接获取钉头、钉尖像素坐标,适合高精度质检。
五、解决你方案里「头尖配对错乱」核心逻辑
不管几级模型,都要加配对逻辑:
1.遍历所有钉头、钉尖;
2.计算每个钉头到所有钉尖的欧式距离;
3.最小距离的一组判定为同一根钉子;
4.过滤距离过大的异常配对(误检杂物)。
六、极简代码示意(两级 YOLO 思路伪代码)
python
运行
from ultralytics import YOLO
import math
# 两级模型
model_nail = YOLO("nail_det.pt") # 一级:检测整根钉子
model_point = YOLO("head_tip.pt") # 二级:检测钉头、钉尖
def calc_pixel_len(p1, p2):
# p1=(x1,y1) 钉头,p2=(x2,y2)钉尖
return math.hypot(p1[0]-p2[0], p1[1]-p2[1])
img = cv2.imread("nail.jpg")
# 第一步:一级检测所有钉子
res_nail = model_nail(img, classes=[0])
nail_boxes = res_nail[0].boxes.xyxy.cpu().numpy()
for box in nail_boxes:
x1, y1, x2, y2 = map(int, box)
# 裁剪单根钉子局部图
crop_img = img[y1:y2, x1:x2]
# 二级识别钉头、钉尖
res_point = model_point(crop_img, classes=[0,1])
boxes = res_point[0].boxes.xyxy.cpu().numpy()
heads = []
tips = []
for b, cls in zip(boxes, res_point[0].boxes.cls):
cx = (b[0]+b[2])/2
cy = (b[1]+b[3])/2
if int(cls) == 0:
heads.append((cx, cy))
else:
tips.append((cx, cy))
# 配对+计算长度
if heads and tips:
length = calc_pixel_len(heads[0], tips[0])
print(f"钉子像素长度:{length:.2f}")
七、最终总结
1.你设计的两级串联 YOLO 方案逻辑成立,可以正常运行,小批量、无重叠钉子场景完全能用;
2.商用流水线优先选择「单模型关键点 YOLO」:速度更快、配对无错乱、测量精度更高、仅维护一套模型;
3.若钉子密集堆叠、背景复杂,必须增加坐标匹配逻辑,否则会出现长度计算错误;
4.如需真实毫米尺寸,拍摄前用标准棋盘格标定相机,得到 mm/像素 换算系数,像素长度 × 系数 = 实物长度。
Q:YOLO 模型在 Android studio 中详细使用步骤及示例代码
A:...
YOLOv8 在 Android Studio 完整落地教程(TFLite 方案,通用、零 NCNN 编译门槛)
整体流程总览
1 Python 将 YOLOv8 导出为 int8.tflite 移动端轻量化模型
2 Android Studio 新建项目,配置 TFLite 推理依赖
3 模型放入 assets 资源目录
4 编写相机预览、图像预处理、TFLite 推理、NMS 后处理、绘制检测框完整代码
5 真机 / 开发板运行实时摄像头 YOLO 检测(水果统计、人流、零件检测通用)
一、电脑端导出 TFLite 模型(Python)
1. 安装依赖
bash
运行
pip install ultralytics
2. 导出代码(量化 int8,大幅提速减体积)
python
运行
from ultralytics import YOLO
# 替换成你的模型 yolov8n.pt / 自定义水果模型
model = YOLO("yolov8n.pt")
# 导出int8量化tflite
model.export(
format="tflite",
int8=True,
imgsz=640,
data="coco128.yaml" # 自定义数据集替换yaml路径
)
执行后生成文件:
yolov8n_int8.tflite 推理模型
labels.txt 类别标签
二、Android Studio 环境配置
1. 新建项目
选择 Empty Activity,语言选 Kotlin(当前 Android 官方主推,附带 Java 注释对照),最低 SDK ≥24。
2. 引入 TFLite 依赖(app/build.gradle Module 级别)
gradle
dependencies {
// Tensorflow Lite 核心推理
implementation 'org.tensorflow:tensorflow-lite:2.15.0'
// GPU加速(可选,大幅提升帧率)
implementation 'org.tensorflow:tensorflow-lite-gpu:2.15.0'
// 支持图片预处理工具
implementation 'org.tensorflow:tensorflow-lite-support:0.4.4'
// 相机X 官方相机预览框架
implementation "androidx.camera:camera-core:1.3.1"
implementation "androidx.camera:camera-camera2:1.3.1"
implementation "androidx.camera:camera-lifecycle:1.3.1"
implementation "androidx.camera:camera-view:1.3.1"
}
同步 Gradle,下载依赖。
3. 创建 assets 文件夹存放模型
1 app/src/main 右键 → New → Directory → 命名 assets
2 将 yolov8n_int8.tflite、labels.txt 复制进 assets
4. 权限配置 AndroidManifest.xml
xml
<!-- 相机权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera.any" />
<!-- 文件读取 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application ...>
<!-- 开启硬件加速渲染 -->
<activity
android:name=".MainActivity"
android:hardwareAccelerated="true">
...
</activity>
</application>
三、核心工具类:YOLO TFLite 推理封装(YoloDetector.kt)
处理:模型加载、图片缩放归一化、推理、NMS 非极大抑制、解析检测框
kotlin
import android.content.Context
import android.graphics.Bitmap
import android.graphics.RectF
import org.tensorflow.lite.Interpreter
import org.tensorflow.lite.support.common.FileUtil
import java.io.BufferedReader
import java.io.InputStreamReader
import java.nio.MappedByteBuffer
import java.util.*
// 检测框数据实体
data class DetectBox(
val x1: Float,
val y1: Float,
val x2: Float,
val y2: Float,
val conf: Float,
val cls: Int
)
class YoloDetector(context: Context) {
companion object {
private const val IMG_SIZE = 640
private const val CONF_THRESH = 0.35f
private const val IOU_THRESH = 0.45f
private const val MODEL_NAME = "yolov8n_int8.tflite"
private const val LABEL_NAME = "labels.txt"
}
private var interpreter: Interpreter
private val labelList = mutableListOf<String>()
init {
// 1. 加载tflite模型
val buffer: MappedByteBuffer = FileUtil.loadMappedFile(context, MODEL_NAME)
val opt = Interpreter.Options().apply {
setNumThreads(4) // 4线程推理,适配开发板/手机
}
interpreter = Interpreter(buffer, opt)
// 2. 加载类别标签
val br = BufferedReader(InputStreamReader(context.assets.open(LABEL_NAME)))
var line: String?
while (br.readLine().also { line = it } != null) {
labelList.add(line!!)
}
br.close()
}
// 图片推理入口
fun detect(bitmap: Bitmap): List<DetectBox> {
// 缩放图片至640*640
val resizeBmp = Bitmap.createScaledBitmap(bitmap, IMG_SIZE, IMG_SIZE, true)
val inputArray = Array(1) { Array(IMG_SIZE) { Array(IMG_SIZE) { FloatArray(3) } } }
// 像素归一化 0~255 -> 0~1
for (y in 0 until IMG_SIZE) {
for (x in 0 until IMG_SIZE) {
val pixel = resizeBmp.getPixel(x, y)
inputArray[0][y][x][0] = (pixel shr 16 and 0xFF) / 255.0f
inputArray[0][y][x][1] = (pixel shr 8 and 0xFF) / 255.0f
inputArray[0][y][x][2] = (pixel and 0xFF) / 255.0f
}
}
// YOLOv8输出维度 [1,8400,num_cls+4]
val numClass = labelList.size
val outputArray = Array(1) { Array(8400) { FloatArray(4 + numClass) } }
interpreter.run(inputArray, outputArray)
// 解析输出
val rawBoxes = mutableListOf<DetectBox>()
val output = outputArray[0]
for (i in 0 until 8400) {
val row = output[i]
val cx = row[0]
val cy = row[1]
val w = row[2]
val h = row[3]
// 取置信度最高类别
var maxConf = 0f
var bestCls = 0
for (c in 0 until numClass) {
val conf = row[4 + c]
if (conf > maxConf) {
maxConf = conf
bestCls = c
}
}
if (maxConf < CONF_THRESH) continue
// 转换左上、右下坐标
val x1 = cx - w / 2
val y1 = cy - h / 2
val x2 = cx + w / 2
val y2 = cy + h / 2
rawBoxes.add(DetectBox(x1, y1, x2, y2, maxConf, bestCls))
}
resizeBmp.recycle()
// NMS去重返回最终框
return nms(rawBoxes)
}
// NMS非极大抑制
private fun nms(boxes: MutableList<DetectBox>): List<DetectBox> {
boxes.sortByDescending { it.conf }
val res = mutableListOf<DetectBox>()
for (box in boxes) {
var keep = true
for (r in res) {
val interX1 = maxOf(box.x1, r.x1)
val interY1 = maxOf(box.y1, r.y1)
val interX2 = minOf(box.x2, r.x2)
val interY2 = minOf(box.y2, r.y2)
val interW = interX2 - interX1
val interH = interY2 - interY1
if (interW <= 0 || interH <= 0) continue
val interArea = interW * interH
val area1 = (box.x2 - box.x1) * (box.y2 - box.y1)
val area2 = (r.x2 - r.x1) * (r.x2 - r.y1)
val iou = interArea / (area1 + area2 - interArea)
if (iou > IOU_THRESH) {
keep = false
break
}
}
if (keep) res.add(box)
}
return res
}
// 释放推理内存
fun close() {
interpreter.close()
}
// 获取类别名称
fun getLabel(clsId: Int): String = labelList[clsId]
}
四、布局页面 activity_main.xml(相机预览 + 绘制层)
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.camera.view.PreviewView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 绘制检测框的覆盖层 -->
<com.example.yolov8android.DrawOverlay
android:id="@+id/drawOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.camera.view.PreviewView>
五、绘制层自定义 View DrawOverlay.kt(画框、文字、统计水果)
kotlin
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
class DrawOverlay @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyle: Int = 0
) : View(context, attrs, defStyle) {
private var boxList = emptyList<DetectBox>()
private var labelMap: (Int) -> String = { "" }
private val paintBox = Paint().apply {
color = Color.GREEN
strokeWidth = 4f
style = Paint.Style.STROKE
}
private val paintText = Paint().apply {
color = Color.YELLOW
textSize = 42f
style = Paint.Style.FILL
}
// 更新检测结果并重绘
fun updateBoxes(boxes: List<DetectBox>, labelFunc: (Int) -> String) {
boxList = boxes
labelMap = labelFunc
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val scaleX = width / 640f
val scaleY = height / 640f
for (box in boxList) {
// 坐标映射到屏幕尺寸
val rect = RectF(
box.x1 * scaleX,
box.y1 * scaleY,
box.x2 * scaleX,
box.y2 * scaleY
)
canvas.drawRect(rect, paintBox)
val text = "${labelMap(box.cls)} ${String.format("%.2f", box.conf)}"
canvas.drawText(text, rect.left, rect.top - 10, paintText)
}
}
}
六、主页面 MainActivity.kt(相机流 + 实时推理)
kotlin
import android.Manifest
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Matrix
import android.media.Image
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import java.nio.ByteBuffer
class MainActivity : AppCompatActivity() {
private lateinit var previewView: PreviewView
private lateinit var drawOverlay: DrawOverlay
private lateinit var detector: YoloDetector
private val REQ_CAMERA = 1001
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
previewView = findViewById(R.id.previewView)
drawOverlay = findViewById(R.id.drawOverlay)
detector = YoloDetector(this)
// 校验相机权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED
) {
startCamera()
} else {
ActivityCompat.requestPermissions(
this, arrayOf(Manifest.permission.CAMERA), REQ_CAMERA
)
}
}
// 启动相机
private fun startCamera() {
val camProviderFuture = ProcessCameraProvider.getInstance(this)
camProviderFuture.addListener({
val cameraProvider = camProviderFuture.get()
// 预览流
val preview = Preview.Builder()
.build()
.also { it.setSurfaceProvider(previewView.surfaceProvider) }
// 图像分析流,每一帧送入YOLO推理
val imageAnalyzer = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also {
it.setAnalyzer(ContextCompat.getMainExecutor(this)) { imageProxy ->
val bitmap = imageToBitmap(imageProxy.image)
val boxes = detector.detect(bitmap)
// 更新绘制层
drawOverlay.updateBoxes(boxes) { cid -> detector.getLabel(cid) }
bitmap.recycle()
imageProxy.close()
}
}
// 后置摄像头
val camSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(this, camSelector, preview, imageAnalyzer)
} catch (e: Exception) {
Log.e("CAM", "相机绑定失败", e)
}
}, ContextCompat.getMainExecutor(this))
}
// Image 转 Bitmap
private fun imageToBitmap(image: Image): Bitmap {
val yBuffer: ByteBuffer = image.planes[0].buffer
val uvBuffer: ByteBuffer = image.planes[1].buffer
val ySize = yBuffer.remaining()
val uvSize = uvBuffer.remaining()
val yData = ByteArray(ySize)
val uvData = ByteArray(uvSize)
yBuffer.get(yData)
uvBuffer.get(uvData)
val width = image.width
val height = image.height
// YUV_420_888 转RGB Bitmap
val yuvImage = android.graphics.YuvImage(yData, image.planes[0].rowStride, width, height, null)
val out = java.io.ByteArrayOutputStream()
yuvImage.compressToJpeg(Rect(0, 0, width, height), 80, out)
val bytes = out.toByteArray()
var bmp = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
// 旋转校正画面
val matrix = Matrix().apply { postRotate(90f) }
bmp = Bitmap.createBitmap(bmp, 0, 0, bmp.width, bmp.height, matrix, true)
return bmp
}
// 权限回调
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<out String>, grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQ_CAMERA && grantResults.isNotEmpty()
&& grantResults[0] == PackageManager.PERMISSION_GRANTED
) {
startCamera()
}
}
override fun onDestroy() {
super.onDestroy()
detector.close()
}
}
七、扩展:水果取货统计逻辑接入 Android
在 ImageAnalysis 回调内增加跟踪统计逻辑:
1 用 HashMap<Int, DetectBox> 缓存每一帧水果 ID
2 连续多帧消失判定客户拿走,用 TextView 展示各类水果累计拿走数量
3 可持久化到本地 SP/CSV 保存营业统计数据
八、常见优化 & 踩坑解决
1帧率低、卡顿
开启 TFLite GPU 代理;推理线程设 4;使用 int8 量化模型;分辨率固定 640
2画面检测框错位
相机预览存在旋转,imageToBitmap 中旋转矩阵角度根据设备调整(90/270)
3模型加载失败
assets 文件夹路径不能错,文件名大小写与代码完全一致
4开发板 RK3568/RK3588 安卓系统
代码通用,可开启 NNAPI 硬件加速,在 Interpreter.Options 增加:
kotlin
val nnApiDelegate = NnApiDelegate()
opt.addDelegate(nnApiDelegate)
5自定义训练模型
替换 tflite 文件 + labels.txt,修改numClass自动适配类别,无需改动推理核心代码
九、替代方案对比
1 TFLite(本文方案):上手最简单、跨所有安卓设备,适合快速开发样机
2 NCNN Android:性能更强、体积更小,但需要交叉编译 C++ 库,开发成本高,适合量产商用设备
3 MNN:国产,国产开发板 NPU 优化优秀,学习成本中等
下一篇:AI人工智能开发技术收集目录结构