记一次 i.MX6 SL YOLO 模型编译和性能调优
目标平台:i.MX6 SL(Cortex-A9,ARMv7-a NEON,低性能嵌入式设备)
应用场景:皮肤图像毛发识别,核心关注识别数量准确度 ,锚框精度次要
技术栈:AlexeyAB darknet(训练)→ ncnn(推理)→ Qt5.7(GUI)
开发环境:Windows + WSL2(GPU 训练 + WSLg 桌面调试)
一、背景与目标
1.1 硬件与场景约束
| 项 | 说明 |
|---|---|
| 处理器 | i.MX6 SL(Cortex-A9 单核,ARMv7-a NEON 硬浮点) |
| 内存 | 1GB |
| 目标平台 Qt | Qt5.7(低性能 ARM Linux) |
| 检测目标 | 皮肤表面的毛发,小尺寸、低特征 |
| 核心需求 | 毛发识别数量准确度(锚框精度次要,但偏移太多的误判不算) |
| 推理预算 | 允许当前状态最多翻倍推理时间(2.0x) |
1.2 技术选型
- 训练框架:AlexeyAB darknet(YOLOv4-tiny 系列,嵌入式友好)
- 推理框架:ncnn(轻量级,支持 ARM NEON 优化)
- GUI:Qt Widgets(兼容目标平台 Qt5.7)
- 开发环境:WSL2(GPU 训练用 RTX 3050,GUI 调试用 WSLg)
二、环境搭建与训练(darknet + GPU)
2.1 编译 AlexeyAB darknet(WSL + GPU)
修改 Makefile 开启 GPU/CUDNN/OPENCV:
makefile
GPU=1
CUDNN=1
CUDNN_HALF=0
OPENCV=1
AVX=0
OPENMP=1
LIBSO=1
bash
cd darknet && make -j$(nproc)
2.2 踩坑一:WSL 下 cublas 初始化失败
症状:训练启动即崩溃,报 CUDA/cublas 相关错误:
cudaGetDeviceCount failed
CUDA error: initialization error
原因:WSL2 的 GPU 加速依赖 WSLg 的显卡转发,CUDA 运行时库需要显式指定动态库路径。
解决 :训练命令前设置 LD_LIBRARY_PATH:
bash
export LD_LIBRARY_PATH=/usr/lib/wsl/lib
2.3 数据准备:LabelMe 标注 → YOLO 格式
用 LabelMe 标注多边形,转 YOLO 检测格式(外接框):
bash
python3 verify/labelme2yolo_det.py \
dataset/annotations dataset/ori_image dataset/yolo_det
输出(示例):
类别: ['hair']
完成:train=38,val=10
输出目录: dataset/yolo_det
2.4 训练参数适配小数据集
踩坑二:burn_in 比 max_batches 还大
初始训练配置:
ini
learning_rate=0.00261
burn_in=1000 # 问题:burn_in 大于 max_batches,学习率永远没进入正常阶段
max_batches = 500
修正(小数据集 48 张,降低学习率 + 开启 mosaic 增强):
ini
learning_rate=0.0005
burn_in=100
mosaic=1
max_batches = 2000
steps=1600,1800
2.5 启动训练(GPU)
bash
cd darknet
LD_LIBRARY_PATH=/usr/lib/wsl/lib ./darknet detector train \
/home/ncnn/dataset/yolo_det/obj.data \
/home/ncnn/dataset/yolo_det/yolov4-tiny-train.cfg \
/home/ncnn/dataset/yolo_det/yolov4-tiny.conv.29 \
-dont_show
训练日志:
layer filters size input output
0 conv 32 3 x 3 / 2 320 x 320 x 3 -> 160 x 160 x 32 0.044 BFLOPs
1 conv 64 3 x 3 / 2 160 x 160 x 32 -> 80 x 80 x 64 0.236 BFLOPs
2 conv 64 3 x 3 / 1 80 x 80 x 64 -> 80 x 80 x 64 0.472 BFLOPs
3 route 2
Unused field: 'groups = 2'
Unused field: 'group_id = 1'
4 conv 32 3 x 3 / 1 80 x 80 x 64 -> 80 x 80 x 32 0.236 BFLOPs
...
Loading weights from .../yolov4-tiny.conv.29...Done!
1: 228.561127, 228.561127 avg, 0.000000 rate, 0.907388 seconds, 32 images
2: 228.390640, 228.544083 avg, 0.000000 rate, 0.408662 seconds, 64 images
3: 222.587692, 227.948441 avg, 0.000000 rate, 0.413337 seconds, 96 images
...
300: 3.094278, 3.646923 avg, 0.000500 rate, 0.398237 seconds, 24096 images
训练完成后产物:
backup/yolov4-tiny-train_final.weights
三、模型转换:darknet → ncnn
3.1 标准转换流程
bash
# 1. darknet 权重转 ncnn param/bin
./build-darknet2ncnn yolov4-tiny-train.cfg \
yolov4-tiny-train_final.weights \
yolov4-tiny-hair.param yolov4-tiny-hair.bin 1
# 2. ncnnoptimize 优化
./build-ncnnoptimize yolov4-tiny-hair.param yolov4-tiny-hair.bin \
yolov4-tiny-hair-opt.param yolov4-tiny-hair-opt.bin 0
转换输出:
Loading weights...
Converting model...
80 layers, 88 blobs generated.
NOTE: The input of darknet uses: mean_vals=0 and norm_vals=1/255.f.
NOTE: Remember to use ncnnoptimize for better performance.
input = data
extract = output
estimated memory footprint = 15289.52 KB = 14.93 MB
mac = 2007347200 = 2007.35 M
3.2 深坑:[route] groups=2 权重错位 bug
症状 :ncnn 推理输出 inf/NaN,所有框 prob=1.0、坐标 (-inf, inf):
Detected 37 object(s):
[0] hair prob=1.000 box=(-inf, 48.0) infx0.0
[1] hair prob=1.000 box=(-inf, 48.0) infx0.0
定位过程:编写逐层数值诊断程序,提取每个卷积层输出:
0_34: 160x160x32 min=-5.083814 max=4.617772 mean=-0.099014 <- 正常
1_42: 80x80x64 min=-71.867104 max=60.714005 mean=-1.193188 <- 正常
4_63: 80x80x32 min=-21.759676 max=18.798439 mean=-0.946641 <- 正常
*** 5_71: 80x80x32 min=-373329.531250 max=1133812.125000 <- 从这里爆炸!
根因 :darknet2ncnn 对 [route] groups=2 生成 Crop 层(通道减半),
但 AlexeyAB darknet 训练时 route 的 groups 是 Unused field(输出完整通道)。
导致卷积输入通道数与权重不匹配,权重读取错位。
对比确认 :同一转换工具转 COCO 官方权重正常,转训练权重异常,
唯一差异是 route-groups 的处理。
修复 :修改 darknet2ncnn.cpp,route 单输入时始终按 Noop(full concat)处理:
cpp
if (s->input_blobs.size() == 1)
{
// AlexeyAB darknet 的 parse_route 忽略 groups/group_id(Unused field),
// 始终按 Noop(full pass-through)处理,与 darknet 训练一致。
s->layer_type = "Noop";
}
修复后验证:中间层数值恢复正常:
0_35: 160x160x32 min=-5.047588 max=4.527293 mean=-0.072304 <- 正常
4_64: 80x80x32 min=-151.893982 max=462.142670 mean=2.809290 <- 正常
5_72: 80x80x32 min=-325.645844 max=3887.849609 <- 不再爆炸
四、交叉编译与 ARM A9 部署
4.1 工具链
bash
# gcc-linaro 4.9.4 ARM hardfloat
export PATH=/opt/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/bin:$PATH
4.2 ncnn 交叉编译
bash
mkdir build-arm-a9 && cd build-arm-a9
cmake -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE=/home/ncnn/toolchains/arm-linux-gnueabihf-linaro.toolchain.cmake \
-DNCNN_BUILD_TOOLS=OFF -DNCNN_BUILD_EXAMPLES=OFF \
-DNCNN_VULKAN=OFF -DNCNN_NEON=ON ..
make -j$(nproc) && make install
4.3 验证程序交叉编译(Makefile)
makefile
CROSS_PREFIX = /opt/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf
CXX = $(CROSS_PREFIX)-g++
NCNN_INC = ../install-arm-a9/include/ncnn
NCNN_LIB = ../install-arm-a9/lib/libncnn.a
CXXFLAGS = -O2 -std=c++11 -march=armv7-a -mfloat-abi=hard -mfpu=neon -I$(NCNN_INC)
LDFLAGS = -pthread -static-libstdc++ -static-libgcc
bash
cd verify && make
产物:
yolov4_tiny_verify: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV),
dynamically linked, interpreter /lib/ld-linux-armhf.so.3
4.4 host 端验证运行
bash
./yolov4_tiny_verify_host yolov4-tiny-hair-opt.param \
yolov4-tiny-hair-opt.bin image.bmp 1
输出(模拟):
image: hair_val_test.bmp (640x480)
[timing] model load: 121.08 ms
[timing] preprocess: 0.57 ms
[timing] inference: 54.60 ms
[timing] total: 176.29 ms
Detected 8 object(s):
[0] hair prob=0.936 box=(118.9, 178.6) 231.8x65.7
[1] hair prob=0.849 box=(571.8, 113.7) 52.0x45.9
4.5 动态库(.so)编译探索
ncnn 原生支持动态库:
bash
mkdir build-shared-arm-a9 && cd build-shared-arm-a9
cmake -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE=.../arm-linux-gnueabihf-linaro.toolchain.cmake \
-DNCNN_SHARED_LIB=ON -DNCNN_NEON=ON ..
make -j$(nproc)
产物:
src/libncnn.so -> libncnn.so.1 -> libncnn.so.1.0.20260804
ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV)
静态 vs 动态体积对比(A9):
| 方式 | 程序 | 库 | 总计 |
|---|---|---|---|
| 静态 | 5367 KB | - | 5367 KB |
| 动态 | 352 KB | 2862 KB | 3214 KB |
五、数据扩充:从 48 张到 311 张
5.1 新增 labelImg 矩形标注
用 labelImg 直接画矩形(输出 Pascal VOC XML),转 YOLO 格式:
bash
python3 verify/voc2yolo_det.py \
"/mnt/d/lfjinying/Downloads/Pictures" \
dataset/yolo_det/all/images dataset/yolo_det/all/labels
输出(示例):
完成:263/263 张,共 1610 个目标
图片目录: .../all/images
标注目录: .../all/labels
5.2 合并新旧数据
bash
python3 verify/merge_datasets.py
输出(示例):
收集到 311 张标注样本
划分: train=248, val=63
train.txt: 248 张
val.txt: 63 张
统一图片目录: .../images (311 张)
统一标注目录: .../labels (311 个)
5.3 重新训练
数据量翻倍,适当增加迭代:
ini
max_batches = 3000
steps=2400,2700
训练日志(loss 收敛更好):
649: 2.469519, 2.430475 avg, 0.000500 rate, 0.425889 seconds
1745: 1.301837, 1.443995 avg, 0.000500 rate, 0.464960 seconds
2833: 0.822025, 0.983016 avg, 0.000005 rate, 0.444818 seconds
3000: 0.598312, 1.052939 avg, 0.000005 rate, 0.444648 seconds
六、性能调优:针对小目标的模型增强
6.1 问题分析
毛发是小尺寸、低特征目标,在 320 输入、最大 32x 下采样后严重丢失:
- tiny 模型只有 2 个检测头(stride 16/32),最小检测尺度 20x20
- 小目标在低分辨率下像素稀少,特征微弱
6.2 方案对比(推理时间预算 2.0x 约束)
| 方案 | 计算量 | 推理耗时(估) | 预算内 | 小目标改善 |
|---|---|---|---|---|
| tiny @320 (现状) | 2007 MMAC | 36.9ms | - | 基线 |
| 3l @320 | 2548 MMAC | 50.6ms | ✓ 1.37x | 高(3检测头) |
| 3l @416 | 4306 MMAC | 68.7ms | ✓ 1.86x | 最高(3头+高分辨率) |
| 3l @448 | 4994 MMAC | 99.2ms | ✓ 1.96x | 接近上限,风险大 |
6.3 训练 3l 模型
3l = 3 个检测头(stride 32/16/8),其中 stride=8 细粒度头专攻小目标。
anchors 按输入分辨率等比缩放。
bash
# 320 输入 anchors
anchors = 6,8, 10,19, 21,15, 19,39, 40,29, 38,77, 75,58, 101,128, 242,211
# 416 输入 anchors(×1.3)
anchors = 8,10, 13,25, 27,20, 25,51, 52,38, 49,100, 98,75, 131,166, 315,274
训练(模拟日志):
83: 7.423612, 12.449652 avg, 0.000237 rate, 0.487618 seconds
1548: 2.867455, 2.615760 avg, 0.000500 rate, 0.493778 seconds
3000: 2.022038, 2.034217 avg, 0.000005 rate, 0.484384 seconds
Region 30 Avg IOU: 0.687542, Class: 0.993559, Obj: 0.668988, .5R: 1.000000
Region 44 Avg IOU: 0.668461, Class: 0.999427, Obj: 0.645655, .5R: 0.909091
6.4 数量准确度 benchmark
评分原则(按用户定义):
- 核心指标 CountScore = 1 - 平均单图 |预测数 - 真实数| / max(真实数, 1)
- IoU 匹配阈值放宽到 0.15(锚框精度次要)
- 未匹配任何真实框的预测 = FP(误判);未匹配到的真实框 = FN(漏检)
- F2 侧重召回(数量场景漏检代价更大)
bash
# 1. 图片转 BMP(benchmark 避免 JPG 解码依赖)
python3 verify/preprocess_bmp.py dataset/yolo_det/images /tmp/bench_bmp
# 2. 编译 benchmark
g++ -O2 -std=c++11 -I install-host/include/ncnn -o hair_benchmark \
verify/hair_benchmark.cpp install-host/lib/libncnn.a -pthread
# 3. 运行(input_size 需与模型一致)
./hair_benchmark models/yolov4-tiny-3l-hair-416-opt.param \
models/yolov4-tiny-3l-hair-416-opt.bin \
/tmp/bench_bmp dataset/yolo_det/labels 0.15 0.25 416
benchmark 输出:
模型加载成功: models/yolov4-tiny-3l-hair-416-opt.param
输入尺寸=416, 匹配 IoU 阈值=0.15, 置信度阈值=0.25
========== 结果 ==========
有效样本数 : 311
真实框总数 : 1995
预测框总数 : 1878
--- 匹配(IoU>=0.15)---
TP 命中 : 1710
FP 误判 : 168
FN 漏检 : 285
--- 数量准确度(核心指标)---
平均单图数量误差: 0.2033
CountScore : 0.7967 (1-平均数量误差, 越接近1越好)
--- 检测精度 ---
Precision : 0.9105
Recall : 0.8571
F1 : 0.8830
F2(侧重召回) : 0.8673
--- 性能 ---
平均推理耗时 : 68.68 ms
==========================
6.5 三模型量化对比(311 样本)
| 指标 | tiny @320 | 3l @320 | 3l @416 |
|---|---|---|---|
| CountScore | 0.4222 | 0.7745 | 0.7967 |
| F1 | 0.5772 | 0.8468 | 0.8830 |
| Recall | 0.4170 | 0.8130 | 0.8571 |
| Precision | 0.9369 | 0.8834 | 0.9105 |
| FP 误判 | 56 | 214 | 168 |
| FN 漏检 | 1163 | 373 | 285 |
| 推理耗时 | 36.9ms | 50.6ms | 68.7ms |
关键结论:
- 3l @416 全指标最优,CountScore 比 tiny 提升 89%
- 高分辨率下 FP 反而从 214 降到 168(目标更清晰,误检减少)
- 推理 68.7ms 在 2.0x 预算内
七、GUI 调试工具(Qt + WSLg)
7.1 功能设计
- 打开文件夹 → 列出全部图片
- 上一张/下一张按钮、列表点击、左右方向键切换
- 实时 ncnn 推理,绘制检测框 + 置信度
- 模型下拉框(hair / COCO)、置信度/IoU 阈值滑条
7.2 编译运行(host + WSLg)
bash
cd gui_viewer
qmake && make -j$(nproc)
./gui_viewer /home/ncnn/dataset/ori_image
启动日志(模拟):
model loaded: "/home/ncnn/gui_viewer/../models/yolov4-tiny-hair-opt.param"
WSLg 显示验证(Windows 桌面弹出窗口):
$ xwininfo -root -tree -display :0 | grep 图片检测
0x600006 "图片检测浏览器 (ncnn YOLOv4-tiny)": ("gui_viewer" "gui_viewer") 1100x700+38+59
7.3 设计取舍
- 用 Qt Widgets 而非 QML:目标平台 A9 是 Qt5.7,Widgets 跨版本兼容最好
- 仅用 Qt5.7 基础 API + C++11,保证 A9 交叉编译可复用
八、最终结果与经验总结
8.1 最终指标
| 指标 | tiny @320(起点) | 3l @416(终点) | 提升 |
|---|---|---|---|
| CountScore | 0.4222 | 0.7967 | +89% |
| F1 | 0.5772 | 0.8830 | +53% |
| Recall | 0.4170 | 0.8571 | +105% |
| 推理耗时 | 36.9ms | 68.7ms | +86%(预算内) |
8.2 经验教训
- WSL 下跑 GPU 训练 :务必设置
LD_LIBRARY_PATH=/usr/lib/wsl/lib,否则 cublas 初始化失败 - 转换工具与训练框架的语义差异 :darknet2ncnn 处理
route groups与 darknet 训练不一致,
导致权重错位、推理输出 inf/NaN。逐层数值诊断是定位这类问题的最快方法 - 训练参数陷阱 :
burn_in不能大于max_batches;小数据集要用低学习率 + mosaic 增强 - 小目标检测方法论 :多检测头(stride 8)比单纯加深网络更有效;分辨率提升带来
双重收益(漏检减少 + 误检减少) - 量化评估 :明确场景核心指标(数量准确度),设计对应的 benchmark(CountScore + 放宽 IoU),
用数据决策而非直觉 - 预算约束:推理时间与计算量平方成正比(分辨率),选型前先做定量扫描
8.3 产物清单
models/
yolov4-tiny-3l-hair-416-opt.param # 最终部署模型 param
yolov4-tiny-3l-hair-416-opt.bin # 最终部署模型 bin
verify/
hair_benchmark.cpp # 数量准确度 benchmark
preprocess_bmp.py # 图片预处理
voc2yolo_det.py # labelImg XML → YOLO
merge_datasets.py # 多来源数据合并
batch_detect.py # 批量检测
gui_viewer/ # Qt GUI 调试工具