记一次 i.MX6 SL YOLO 模型编译和性能调优

记一次 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_inmax_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 经验教训

  1. WSL 下跑 GPU 训练 :务必设置 LD_LIBRARY_PATH=/usr/lib/wsl/lib,否则 cublas 初始化失败
  2. 转换工具与训练框架的语义差异 :darknet2ncnn 处理 route groups 与 darknet 训练不一致,
    导致权重错位、推理输出 inf/NaN。逐层数值诊断是定位这类问题的最快方法
  3. 训练参数陷阱burn_in 不能大于 max_batches;小数据集要用低学习率 + mosaic 增强
  4. 小目标检测方法论 :多检测头(stride 8)比单纯加深网络更有效;分辨率提升带来
    双重收益(漏检减少 + 误检减少)
  5. 量化评估 :明确场景核心指标(数量准确度),设计对应的 benchmark(CountScore + 放宽 IoU),
    用数据决策而非直觉
  6. 预算约束:推理时间与计算量平方成正比(分辨率),选型前先做定量扫描

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 调试工具
相关推荐
一碗白开水一15 小时前
入门实践工程二:基于 YOLOv8 的日常物品目标检测|附:环境依赖环境及工程源码
yolo·目标检测·目标跟踪
探物 AI20 小时前
yolov5里面的骨干组件14:C3模块 (CSP Bottleneck with 3 Convolutions)
yolo
雪的季节1 天前
不安装YOLO只安装 PyTorch,加载已有yolo数据,从无到有创建模型训练数据并加载使用(重要)
人工智能·pytorch·yolo
guo_xiao_xiao_1 天前
YOLO[室内与室外多场景混凝土块目标检测]目标检测数据集
yolo·目标检测·目标跟踪
江畔柳前堤1 天前
YOLO 目标检测全流程深度剖析
人工智能·yolo·目标检测·计算机视觉·unity·面试·vllm
guo_xiao_xiao_1 天前
YOLO[野外溪流与人工水域][鱼]目标检测数据集
yolo·目标检测·目标跟踪
智购科技智能售货柜1 天前
自动售货机商品识别YOLO模型训练实战:从6万张图片到98%识别率的完整复盘~YH
运维·服务器·数据库·人工智能·redis·物联网·yolo
CoderIsArt1 天前
SAHI with YOLOv5 for Sliced Inference
人工智能·yolo
jay神2 天前
深度学习确定baseline之后怎么做改进?
人工智能·深度学习·yolo·计算机视觉·分类